pr_7_payback_of_advertising¶
Анализ бизнес-показателей - окупаемость рекламы¶
Описание проекта¶
Несмотря на огромные вложения в рекламу нашего мобильного приложения, последние несколько месяцев компания терпит убытки. Разберемся в причинах, чтобы помочь компании выйти в плюс.
Поймем:
- откуда приходят пользователи и какими устройствами они пользуются;
- сколько стоит привлечение пользователей из различных рекламных каналов;
- сколько денег приносит каждый клиент;
- когда расходы на привлечение клиента окупаются;
- какие факторы мешают привлечению клиентов.
Ход исследования:
Располагаем данными о пользователях, привлечённых с 1 мая по 27 октября 2019 года:
- лог сервера с данными об их посещениях,
- выгрузка покупок пользователей за этот период,
- рекламные расходы.
О качестве данных ничего не известно, поэтому перед тем, как приступить к исследованию, понадобится обзор данных. Таким образом, исследование пройдет через четыре этапа:
- Обзор данных.
- Предобработка данных.
- Исследовательский анализ данных.
- Написание общего вывода.
Получение и подготовка данных¶
Подключим библиотеки.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.style
import seaborn as sns
from datetime import datetime, timedelta
Загрузим данные и подготовим их к анализу.
Загрузим данные о визитах, заказах и рекламных расходах из CSV-файлов в переменные.
try:
visits = pd.read_csv('datasets/visits_info_short.csv')
orders = pd.read_csv('datasets/orders_info_short.csv')
ad_costs = pd.read_csv('datasets/costs_info_short.csv')
except:
print('Файл не найден. Проверьте путь.')
Познакомимся с данными.
df_ref={'visits': visits, 'orders': orders, 'ad_costs': ad_costs}
for name, df in df_ref.items():
print(50*'*', '\n', name, '\n', 50*'*')
df.info()
print(50*'*')
display(df.head(3))
************************************************** visits ************************************************** <class 'pandas.core.frame.DataFrame'> RangeIndex: 309901 entries, 0 to 309900 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 User Id 309901 non-null int64 1 Region 309901 non-null object 2 Device 309901 non-null object 3 Channel 309901 non-null object 4 Session Start 309901 non-null object 5 Session End 309901 non-null object dtypes: int64(1), object(5) memory usage: 14.2+ MB **************************************************
| User Id | Region | Device | Channel | Session Start | Session End | |
|---|---|---|---|---|---|---|
| 0 | 981449118918 | United States | iPhone | organic | 2019-05-01 02:36:01 | 2019-05-01 02:45:01 |
| 1 | 278965908054 | United States | iPhone | organic | 2019-05-01 04:46:31 | 2019-05-01 04:47:35 |
| 2 | 590706206550 | United States | Mac | organic | 2019-05-01 14:09:25 | 2019-05-01 15:32:08 |
************************************************** orders ************************************************** <class 'pandas.core.frame.DataFrame'> RangeIndex: 40212 entries, 0 to 40211 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 User Id 40212 non-null int64 1 Event Dt 40212 non-null object 2 Revenue 40212 non-null float64 dtypes: float64(1), int64(1), object(1) memory usage: 942.6+ KB **************************************************
| User Id | Event Dt | Revenue | |
|---|---|---|---|
| 0 | 188246423999 | 2019-05-01 23:09:52 | 4.99 |
| 1 | 174361394180 | 2019-05-01 12:24:04 | 4.99 |
| 2 | 529610067795 | 2019-05-01 11:34:04 | 4.99 |
************************************************** ad_costs ************************************************** <class 'pandas.core.frame.DataFrame'> RangeIndex: 1800 entries, 0 to 1799 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 dt 1800 non-null object 1 Channel 1800 non-null object 2 costs 1800 non-null float64 dtypes: float64(1), object(2) memory usage: 42.3+ KB **************************************************
| dt | Channel | costs | |
|---|---|---|---|
| 0 | 2019-05-01 | FaceBoom | 113.3 |
| 1 | 2019-05-02 | FaceBoom | 78.1 |
| 2 | 2019-05-03 | FaceBoom | 85.8 |
В данных отсутствуют пропуски. Названия колонок не приведено к стандарту. Исправим.
for name, df in df_ref.items():
df.columns = [x.lower().replace(' ', '_').lstrip().rstrip() for x in df.columns]
Исправим типы данных для колонок с датой и временем.
visits.session_start = pd.to_datetime(visits.session_start)
visits.session_end = pd.to_datetime(visits.session_end)
orders.event_dt = pd.to_datetime(orders.event_dt)
ad_costs.dt = pd.to_datetime(ad_costs.dt)
Посмотрим на результат.
df_ref={'visits': visits, 'orders': orders, 'ad_costs': ad_costs}
for name, df in df_ref.items():
print(50*'*', '\n', name, '\n', 50*'*')
df.info()
print(50*'*')
display(df.head(3))
************************************************** visits ************************************************** <class 'pandas.core.frame.DataFrame'> RangeIndex: 309901 entries, 0 to 309900 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 309901 non-null int64 1 region 309901 non-null object 2 device 309901 non-null object 3 channel 309901 non-null object 4 session_start 309901 non-null datetime64[ns] 5 session_end 309901 non-null datetime64[ns] dtypes: datetime64[ns](2), int64(1), object(3) memory usage: 14.2+ MB **************************************************
| user_id | region | device | channel | session_start | session_end | |
|---|---|---|---|---|---|---|
| 0 | 981449118918 | United States | iPhone | organic | 2019-05-01 02:36:01 | 2019-05-01 02:45:01 |
| 1 | 278965908054 | United States | iPhone | organic | 2019-05-01 04:46:31 | 2019-05-01 04:47:35 |
| 2 | 590706206550 | United States | Mac | organic | 2019-05-01 14:09:25 | 2019-05-01 15:32:08 |
************************************************** orders ************************************************** <class 'pandas.core.frame.DataFrame'> RangeIndex: 40212 entries, 0 to 40211 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 40212 non-null int64 1 event_dt 40212 non-null datetime64[ns] 2 revenue 40212 non-null float64 dtypes: datetime64[ns](1), float64(1), int64(1) memory usage: 942.6 KB **************************************************
| user_id | event_dt | revenue | |
|---|---|---|---|
| 0 | 188246423999 | 2019-05-01 23:09:52 | 4.99 |
| 1 | 174361394180 | 2019-05-01 12:24:04 | 4.99 |
| 2 | 529610067795 | 2019-05-01 11:34:04 | 4.99 |
************************************************** ad_costs ************************************************** <class 'pandas.core.frame.DataFrame'> RangeIndex: 1800 entries, 0 to 1799 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 dt 1800 non-null datetime64[ns] 1 channel 1800 non-null object 2 costs 1800 non-null float64 dtypes: datetime64[ns](1), float64(1), object(1) memory usage: 42.3+ KB **************************************************
| dt | channel | costs | |
|---|---|---|---|
| 0 | 2019-05-01 | FaceBoom | 113.3 |
| 1 | 2019-05-02 | FaceBoom | 78.1 |
| 2 | 2019-05-03 | FaceBoom | 85.8 |
Описание данных¶
visits - лог сервера с информацией о посещениях сайта:
user_id— уникальный идентификатор пользователя;region— страна пользователя;device— тип устройства пользователя;channel— идентификатор источника перехода;session_start— дата и время начала сессии;session_end— дата и время окончания сессии.
orders - информация о заказах:
user_id— уникальный идентификатор пользователя;event_dt— дата и время покупки;revenue— сумма заказа.
ad_costs - информацию о расходах на рекламу:
dt— дата проведения рекламной кампании;channel— идентификатор рекламного источника;costs— расходы на эту кампанию.
Проверим данные на отсутствие явных дубликатов¶
for item in [orders, visits, ad_costs]:
print(item.duplicated().sum())
0 0 0
Явные дубликаты в данных отсутствуют.
Посмотрим на уникальные значения в столбцах.
for item in [visits.region, visits.device, visits.channel, ad_costs.channel]:
display(item.name, item.unique(), len(item.unique()))
print('*'*50)
'region'
array(['United States', 'UK', 'France', 'Germany'], dtype=object)
4
**************************************************
'device'
array(['iPhone', 'Mac', 'Android', 'PC'], dtype=object)
4
**************************************************
'channel'
array(['organic', 'TipTop', 'RocketSuperAds', 'YRabbit', 'FaceBoom',
'MediaTornado', 'AdNonSense', 'LeapBob', 'WahooNetBanner',
'OppleCreativeMedia', 'lambdaMediaAds'], dtype=object)
11
**************************************************
'channel'
array(['FaceBoom', 'MediaTornado', 'RocketSuperAds', 'TipTop', 'YRabbit',
'AdNonSense', 'LeapBob', 'OppleCreativeMedia', 'WahooNetBanner',
'lambdaMediaAds'], dtype=object)
10
**************************************************
Проверим, каки даты представлены в данных.
for t, item in {'visits': visits.session_start, 'visits': visits.session_end,
'orders': orders.event_dt,
'ad_costs': ad_costs.dt}.items():
print(t)
print(item.name, '\t', item.min(), '\t', item.max(), '\n', '*'*60)
visits session_end 2019-05-01 00:07:06 2019-11-01 01:38:46 ************************************************************ orders event_dt 2019-05-01 00:28:11 2019-10-31 23:56:56 ************************************************************ ad_costs dt 2019-05-01 00:00:00 2019-10-27 00:00:00 ************************************************************
- Посещения пользователей выполнялись с 1 мая 2019 года до 1 ноября 2019 года.
- Заказы пользователей размещались с 1 мая по 31 октября 2019 года.
- Оплата рекламы выполнялась с 1 мая до 27 октября 2019 год.
Добавим в датафрейм ad_costs столбцы с номером недели в году и номером месяца.
ad_costs['week'] = ad_costs['dt'].dt.isocalendar().week
ad_costs['month'] = ad_costs['dt'].dt.month
ad_costs.head()
| dt | channel | costs | week | month | |
|---|---|---|---|---|---|
| 0 | 2019-05-01 | FaceBoom | 113.3 | 18 | 5 |
| 1 | 2019-05-02 | FaceBoom | 78.1 | 18 | 5 |
| 2 | 2019-05-03 | FaceBoom | 85.8 | 18 | 5 |
| 3 | 2019-05-04 | FaceBoom | 136.4 | 18 | 5 |
| 4 | 2019-05-05 | FaceBoom | 122.1 | 18 | 5 |
Вывод
- Дубликаты и пропуски в данных отсутствуют.
- Названия столбцов приведены к стандарту.
- Преобразован тип данных для столбцов с информацией о датах и времени.
Объявление функций¶
Функции для вычисления значений метрик, вывода табличных данных, построения графиков.
get_profiles() — для создания профилей пользователей¶
# функция для создания пользовательских профилей
def get_profiles(sessions, orders, events, ad_costs, event_names=[]):
'''
Функция для создания пользовательских профилей.
Получает:
sessions - датафрейм с информацией о сессиях пользователя
orders - датафрейм с информацией о заказах пользователя
events - датафрейм с информацией о действиях пользователя на сайте или в приложении
ad_costs - датафрей с информацией о расходах на рекламу
event_name - срезы данных
Возвращает:
profiles - профили пользователей
'''
# находим параметры первых посещений
profiles = (
sessions.sort_values(by=['user_id', 'session_start'])
.groupby('user_id')
.agg(
{
'session_start': 'first',
'channel': 'first',
'device': 'first',
'region': 'first',
}
)
.rename(columns={'session_start': 'first_ts'})
.reset_index()
)
# для когортного анализа определяем дату первого посещения
# и первый день месяца, в который это посещение произошло
profiles['dt'] = profiles['first_ts'].dt.date
profiles['dt'] = pd.to_datetime(profiles['dt'])
profiles['month'] = profiles['first_ts'].values.astype('datetime64[M]')
# добавляем признак платящих пользователей
profiles['payer'] = profiles['user_id'].isin(orders['user_id'].unique())
# добавляем флаги для всех событий из event_names
for event in event_names:
if event in events['event_name'].unique():
profiles[event] = profiles['user_id'].isin(
events.query('event_name == @event')['user_id'].unique()
)
# считаем количество уникальных пользователей
# с одинаковыми источником и датой привлечения
new_users = (
profiles.groupby(['dt', 'channel'])
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'unique_users'})
.reset_index()
)
# print('new_users', '\n', new_users.info())
# print('ad_costs', '\n', ad_costs.info())
# print('profiles', '\n', profiles.info())
# объединяем траты на рекламу и число привлечённых пользователей
ad_costs = ad_costs.merge(new_users, on=['dt', 'channel'], how='left')
# делим рекламные расходы на число привлечённых пользователей
ad_costs['acquisition_cost'] = ad_costs['costs'] / ad_costs['unique_users']
# print(profiles.head())
# print(ad_costs.head())
# добавляем стоимость привлечения в профили
profiles = profiles.merge(
ad_costs[['dt', 'channel', 'acquisition_cost']],
on=['dt', 'channel'],
how='left',
)
# стоимость привлечения органических пользователей равна нулю
profiles['acquisition_cost'] = profiles['acquisition_cost'].fillna(0)
return profiles
Проверим работу функции
# profiles = get_profiles(visits, orders, orders, ad_costs)
# profiles
profile_ratio() — для подсчета доли пользователей или платящих пользователей¶
# функция для расчета доли пользователей или платящих пользователей
def profile_ratio(df, group='region', payer=False):
'''
Функция для вычисления количества уникальных значений в столбце и доли этих значений.
Данные группируются по столбцу group.
df - датафрейм для вычислений (df=profiles)
group - группировка значений
payer - считать ли платящих пользователей (True) или всех пользователей (False)
'''
if payer:
result = df.query('payer').groupby(group).agg({'user_id': 'nunique'})
else:
result = df.groupby(group).agg({'user_id': 'nunique'})
result['ratio'] = result['user_id'] / result['user_id'].sum()
if payer:
ratio_name = 'ratio_payers'
result.columns = ['payers', ratio_name]
else:
ratio_name = 'ratio_users'
result.columns = ['users', ratio_name]
return result.sort_values(by=ratio_name, ascending=False)
Проверим работу функции
# profile_ratio(df=profiles, group='device', payer=False).style.format({ratio_name: '{:.1%}'})
# profile_ratio(df=profiles, group='region', payer=True).style.format({ratio_name: '{:.1%}'})
Функция работате корректно.
profile_cr() — для подсчета конверсии пользователей в платящих пользователей¶
# функция для подсчета конверсии пользователей в платящих пользователей
def profile_cr(df, group='region'):
'''
Функция для вычисления конверсии пользователей в платящих пользователей
Данные группируются по столбцу group.
df - датафрейм для вычислений
group - группировка значений
'''
# Количество пользователей и платящих пользователей
result = profiles.groupby(group).agg({'user_id': 'nunique', 'payer': 'sum'})
# Переименуем колонки
result.columns = ['users', 'payers']
# Посчитаем конверсию
result['cr'] = result['payers'] / result['users']
# Отсортитуем данные и зададим формат вывод конверсии.
return result.sort_values(by='cr', ascending=False)
Проверим работу функции.
# profile_cr(df=profiles, group='device').style.format({'cr': '{:.1%}'})
Функция работате корректно.
disp_df() — для вывода на экран с процентами в последнем столбце¶
def disp_df(df):
'''
Функция выводит на экран датафрейм. Последней колонке задает формат вывода в виде процентов.
'''
col = df.columns[-1]
display(df.style.format({col: '{:.1%}'}))
disp() — для вывода на экран таблиц конверсии, увержания, ROI¶
def disp(df, horizon_days=14):
'''
Функция форматирует вывод на экран.
Процентные величины выводятся с одним знаком после запятой.
Сортировка по последней колонке.
'''
try:
display(
(df.sort_values(by=(horizon_days - 1), ascending=False)
.style
.format(formatter='{:.1%}', subset=list(range(horizon_days)))
.format(subset=['cohort_size'], precision=0)
.format_index(axis=1, precision=0)
.format(formatter='$ {:.02f}', subset=['cac'])
)
)
except:
display(
(df.sort_values(by=(horizon_days - 1), ascending=False)
.style
.format(formatter='{:.1%}', subset=list(range(horizon_days)))
.format(subset=['cohort_size'], precision=0)
.format_index(axis=1, precision=0)
)
)
disp_no_sort() — для вывода на экран таблиц конверсии, ROI без сортировки¶
def disp_no_sort(df):
'''
Функция форматирует вывод на экран.
Процентные величины выводятся с одним знаком после запятой.
Сортировка по 13 колонке.
'''
try:
display(
(df.style
.format(formatter='{:.1%}', subset=list(range(horizon_days)))
.format(subset=['cohort_size'], precision=0)
.format_index(axis=1, precision=0)
.format(formatter='$ {:.02f}', subset=['cac'])
)
)
except:
display(
(df.style
.format(formatter='{:.1%}', subset=list(range(horizon_days)))
.format(subset=['cohort_size'], precision=0)
.format_index(axis=1, precision=0)
)
)
Проверим работу функции.
# print('ltv_region_channel')
# disp_no_sort(roi_region_channel)
Функция работает корректно.
get_retention() — для подсчёта Retention Rate¶
# функция для расчёта удержания
def get_retention(
profiles,
sessions,
observation_date,
horizon_days,
dimensions=[],
ignore_horizon=False,
):
'''
Функция для расчета удержания пользователей (Retention Rate).
Получает:
profiles - профили пользователей
sessions - сессии пользователей
observation_date - дата на которую выполняется анализ
horizon_days - количество дней lifetime, для которых выполняем анализ, int, одна неделя = 7, две = 14
dimensions=[] - срезы данных
ignore_horizon=False - если True, выполнить расчет без учета горизонта анализа. Ведет к занижению результатов!
Возвращает:
result_raw - сырые данные по удержанию для проверок расчетов
result_grouped - сгруппированные данные по удержанию
result_in_time - данные по удержанию во времени.
'''
# добавляем столбец payer в передаваемый dimensions список
dimensions = ['payer'] + dimensions
# исключаем пользователей, не «доживших» до горизонта анализа
last_suitable_acquisition_date = observation_date
if not ignore_horizon:
last_suitable_acquisition_date = observation_date - timedelta(
# days=horizon_days - 1
days=horizon_days # Если дата анализ 1 ноября, то 19 октября не должно попасть в выборку.
)
# Убираем из анализа все слишком новые профили: дата первого визита попадает в горизонт анализа от даты создания отчета
result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
# dt - дата первого посещения приложения пользователем.
# собираем «сырые» данные для расчёта удержания
# соединяем нужные профили с информациях о визитах этих пользователей
result_raw = result_raw.merge(
sessions[['user_id', 'session_start']], on='user_id', how='left'
)
# для каждого визита вычисляем срок "жизни" пользователя от его первого посещения приложения
result_raw['lifetime'] = (
result_raw['session_start'] - result_raw['first_ts']
).dt.days
# функция для группировки таблицы по желаемым признакам
def group_by_dimensions(df, dims, horizon_days):
# группируем данные по переданному индексу и lifitime
result = df.pivot_table(
index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
)
# Вычисляем размер когорт
cohort_sizes = (
df.groupby(dims)
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'cohort_size'})
)
# соединяем информацию о размере когорт и lifetime
result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
# делим количество пользователей в каждом lifetime на размер когорты
result = result.div(result['cohort_size'], axis=0)
# Формируем выборку по максимальному количеству lifetime, которые нам интересны.
result = result[['cohort_size'] + list(range(horizon_days))]
# Восстанавливаем данные по размеру когорт
result['cohort_size'] = cohort_sizes
# display(result)
return result
# получаем таблицу удержания
result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)
# получаем таблицу динамики удержания
result_in_time = group_by_dimensions(
result_raw, dimensions + ['dt'], horizon_days
)
# возвращаем обе таблицы и сырые данные
return result_raw, result_grouped, result_in_time
Проверим работу функции. Если анализ выполняем 1 ноября, то 19 октября не должно попасть в анализ.
# retention_raw, retention, retention_history = get_retention(profiles, visits, observation_date, horizon_days)
# display(observation_date)
# display(horizon_days)
# display(retention_raw.head(2))
# display(retention_raw['first_ts'].max())
# display(retention)
# display(retention_history.head(2))
Функция работает корректно. В анализ попадают пользователи до 18 октября включительно. Расчитано 14 лайвтаймов с 0 по 13.
get_conversion() — для подсчёта конверсии¶
# функция для расчёта конверсии
def get_conversion(
profiles,
purchases,
observation_date,
horizon_days,
dimensions=[],
ignore_horizon=False,
):
'''
Функция для расчёта конверсии (Convertion Rate)
Получает:
profiles - профили пользователей
purchases - заказы пользователей (покупки)
observation_date - дата на которую выполняется анализ
horizon_days - количество дней lifetime, для которых выполняем анализ, int, одна неделя = 7, две = 14
dimensions=[] - срезы данных
ignore_horizon=False - если True, выполнить расчет без учета горизонта анализа. Ведет к занижению результатов!
Возвращает:
result_raw - сырые данные по конверсии для проверок расчетов
result_grouped - сгруппированные данные по конверсии
result_in_time - данные по конверсии во времени.
'''
# исключаем пользователей, не «доживших» до горизонта анализа
last_suitable_acquisition_date = observation_date
if not ignore_horizon:
last_suitable_acquisition_date = observation_date - timedelta(
# days=horizon_days - 1
days=horizon_days # Если дата анализ 1 ноября, то 19 октября не должно попасть в выборку.
)
# display(last_suitable_acquisition_date)
result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
# определяем дату и время первой покупки для каждого пользователя
first_purchases = (
purchases.sort_values(by=['user_id', 'event_dt'])
.groupby('user_id')
.agg({'event_dt': 'first'})
.reset_index()
)
# добавляем данные о покупках в профили
result_raw = result_raw.merge(
first_purchases[['user_id', 'event_dt']], on='user_id', how='left'
)
# рассчитываем лайфтайм для каждой покупки
result_raw['lifetime'] = (
result_raw['event_dt'] - result_raw['first_ts']
).dt.days
# группируем по cohort, если в dimensions ничего нет
if len(dimensions) == 0:
result_raw['cohort'] = 'All users'
dimensions = dimensions + ['cohort']
# функция для группировки таблицы по желаемым признакам
def group_by_dimensions(df, dims, horizon_days):
result = df.pivot_table(
index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
)
result = result.fillna(0).cumsum(axis = 1)
cohort_sizes = (
df.groupby(dims)
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'cohort_size'})
)
result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
# делим каждую «ячейку» в строке на размер когорты
# и получаем conversion rate
result = result.div(result['cohort_size'], axis=0)
result = result[['cohort_size'] + list(range(horizon_days))]
result['cohort_size'] = cohort_sizes
return result
# получаем таблицу конверсии
result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)
# для таблицы динамики конверсии убираем 'cohort' из dimensions
if 'cohort' in dimensions:
dimensions = []
# получаем таблицу динамики конверсии
result_in_time = group_by_dimensions(
result_raw, dimensions + ['dt'], horizon_days
)
# возвращаем обе таблицы и сырые данные
return result_raw, result_grouped, result_in_time
Проверим работу функции. Если дата анализ 1 ноября, то 19 октября не должно попасть в выборку.
# conversion_raw, conversion, conversion_history = get_conversion(profiles, orders, observation_date, horizon_days)
# display(observation_date)
# display(horizon_days)
# display(conversion_raw.head(2))
# display(conversion_raw['first_ts'].max())
# display(conversion)
# conversion_history.head(2)
Функция работает корректно. В выборке 14 лайвтаймов с 0 по 13. Отобраны профили пользователей, созданные до 18 октября включительно.
get_ltv() — для подсчёта LTV¶
# функция для расчёта LTV и ROI
def get_ltv(
profiles,
purchases,
observation_date,
horizon_days,
dimensions=[],
ignore_horizon=False,
):
'''
Функция для расчёта LTV (Lifetime Value) и ROI (Return on Investment)
Получает:
profiles - профили пользователей
purchases - заказы пользователей (покупки)
observation_date - дата на которую выполняется анализ
horizon_days - количество дней lifetime, для которых выполняем анализ, int, одна неделя = 7, две = 14
dimensions=[] - срезы данных
ignore_horizon=False - если True, выполнить расчет без учета горизонта анализа. Ведет к занижению результатов!
Возвращает:
result_raw - сырые данные
result_grouped - таблица LTV
result_in_time - таблица динамики LTV во времени
roi_grouped - таблица ROI
roi_in_time - таблица динамики ROI во времени
'''
# исключаем пользователей, не «доживших» до горизонта анализа
last_suitable_acquisition_date = observation_date
if not ignore_horizon:
last_suitable_acquisition_date = observation_date - timedelta(
days=horizon_days # Если дата анализ 1 ноября, то 19 октября не должно попасть в выборку.
# days=horizon_days - 1
)
result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
# добавляем данные о покупках в профили
result_raw = result_raw.merge(
purchases[['user_id', 'event_dt', 'revenue']], on='user_id', how='left'
)
# рассчитываем лайфтайм пользователя для каждой покупки
result_raw['lifetime'] = (
result_raw['event_dt'] - result_raw['first_ts']
).dt.days
# группируем по cohort, если в dimensions ничего нет
if len(dimensions) == 0:
result_raw['cohort'] = 'All users'
dimensions = dimensions + ['cohort']
# функция группировки по желаемым признакам
def group_by_dimensions(df, dims, horizon_days):
# строим «треугольную» таблицу выручки
result = df.pivot_table(
index=dims, columns='lifetime', values='revenue', aggfunc='sum'
)
# находим сумму выручки с накоплением
result = result.fillna(0).cumsum(axis=1)
# вычисляем размеры когорт
cohort_sizes = (
df.groupby(dims)
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'cohort_size'})
)
# объединяем размеры когорт и таблицу выручки
result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
# считаем LTV: делим каждую «ячейку» в строке на размер когорты
result = result.div(result['cohort_size'], axis=0)
# исключаем все лайфтаймы, превышающие горизонт анализа
result = result[['cohort_size'] + list(range(horizon_days))]
# восстанавливаем размеры когорт
result['cohort_size'] = cohort_sizes
# собираем датафрейм с данными пользователей и значениями CAC,
# добавляя параметры из dimensions
cac = df[['user_id', 'acquisition_cost'] + dims].drop_duplicates()
# считаем средний CAC по параметрам из dimensions
cac = (
cac.groupby(dims)
.agg({'acquisition_cost': 'mean'})
.rename(columns={'acquisition_cost': 'cac'})
)
# считаем ROI: делим LTV на CAC
roi = result.div(cac['cac'], axis=0)
# удаляем строки с бесконечным ROI
roi = roi[~roi['cohort_size'].isin([np.inf])]
# восстанавливаем размеры когорт в таблице ROI
roi['cohort_size'] = cohort_sizes
# добавляем CAC в таблицу ROI
roi['cac'] = cac['cac']
# в финальной таблице оставляем размеры когорт, CAC
# и ROI в лайфтаймы, не превышающие горизонт анализа
roi = roi[['cohort_size', 'cac'] + list(range(horizon_days))]
# возвращаем таблицы LTV и ROI
return result, roi
# получаем таблицы LTV и ROI
result_grouped, roi_grouped = group_by_dimensions(
result_raw, dimensions, horizon_days
)
# для таблиц динамики убираем 'cohort' из dimensions
if 'cohort' in dimensions:
dimensions = []
# получаем таблицы динамики LTV и ROI
result_in_time, roi_in_time = group_by_dimensions(
result_raw, dimensions + ['dt'], horizon_days
)
return (
result_raw, # сырые данные
result_grouped, # таблица LTV
result_in_time, # таблица динамики LTV
roi_grouped, # таблица ROI
roi_in_time, # таблица динамики ROI
)
Проверим работу функции
# ltv_dev_raw, ltv_dev, ltv_dev_history, roi_dev, roi_dev_history = get_ltv(profiles, orders,
# observation_date, horizon_days, dimensions=['device'])
# display(observation_date)
# display(horizon_days)
# display(ltv_dev_raw.head(2))
# display(ltv_dev_raw['first_ts'].max())
# display(ltv_dev)
# display(ltv_dev_history.head(2))
# display(roi_dev)
# display(roi_dev_history.head(2))
Функция работает корректно. В выборке 14 лайвтаймов с 0 по 13. Отобраны профили пользователей, созданные до 18 октября включительно.
Функции для построения графиков:
filter_data() — для сглаживания данных¶
# функция для сглаживания фрейма
def filter_data(df, window):
'''
Функция для сглаживания фрейма.
df - датафрейм для сглаживания
window - ширина окна для сглаживания, например 7, int
'''
# для каждого столбца применяем скользящее среднее
for column in df.columns.values:
df[column] = df[column].rolling(window).mean()
return df
plot_retention() — для построения графика Retention Rate¶
# функция для визуализации удержания
def plot_retention(retention, retention_history, horizon, window=7):
'''
Функция для визуализации удержания.
Получает:
retention - сгруппированные данные по удержанию
retention_history - данные по удержанию во времени
horizon_days - количество дней lifetime, для которых выполняем анализ, int, одна неделя = 7, две = 14
window - ширина "окна" сглаживания, int, например 4.
Возвращает:
кривые удержания платящих пользователей
кривые удержания неплатящих пользователей
динамику удержания платящих пользователей
динамику удержания неплатящих пользователей
'''
# задаём размер сетки для графиков
plt.figure(figsize=(15, 10))
# исключаем размеры когорт и удержание первого дня
retention = retention.drop(columns=['cohort_size', 0])
# в таблице динамики оставляем только нужный лайфтайм
retention_history = retention_history.drop(columns=['cohort_size'])[
[horizon - 1]
]
# display(retention_history.head())
# если в индексах таблицы удержания только payer,
# добавляем второй признак — cohort
if retention.index.nlevels == 1:
retention['cohort'] = 'All users'
retention = retention.reset_index().set_index(['cohort', 'payer'])
# в таблице графиков — два столбца и две строки, четыре ячейки
# в первой строим кривые удержания платящих пользователей
ax1 = plt.subplot(2, 2, 1)
result = retention.query('payer == True').droplevel('payer')
# display(result)
result.iloc[:6].T.plot(grid=True, ax=ax1, linestyle='-')
try:
result.iloc[6:].T.plot(grid=True, ax=ax1, linestyle='--')
except:
pass
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('Удержание платящих пользователей')
# во второй ячейке строим кривые удержания неплатящих
# вертикальная ось — от графика из первой ячейки
ax2 = plt.subplot(2, 2, 2, sharey=ax1)
result = retention.query('payer == False').droplevel('payer')
result.iloc[:6].T.plot(grid=True, ax=ax2, linestyle='-')
try:
result.iloc[6:].T.plot(grid=True, ax=ax2, linestyle='--')
except:
pass
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('Удержание неплатящих пользователей')
# в третьей ячейке — динамика удержания платящих
ax3 = plt.subplot(2, 2, 3)
# получаем названия столбцов для сводной таблицы
columns = [
name
for name in retention_history.index.names
if name not in ['dt', 'payer']
]
# фильтруем данные и строим график
filtered_data = retention_history.query('payer == True').pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
result = filter_data(filtered_data, window)
result.iloc[:, :6].plot(grid=True, ax=ax3, linestyle='-')
try:
result.iloc[:, 6:].plot(grid=True, ax=ax3, linestyle='--')
except:
pass
plt.xlabel('Дата привлечения')
plt.title(
'Динамика удержания платящих пользователей на {}-й день'.format(
horizon
)
)
# в четвертой ячейке — динамика удержания неплатящих
ax4 = plt.subplot(2, 2, 4, sharey=ax3)
# фильтруем данные и строим график
filtered_data = retention_history.query('payer == False').pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
result = filter_data(filtered_data, window)
result.iloc[:, :6].plot(grid=True, ax=ax4, linestyle='-')
try:
result.iloc[:, 6:].plot(grid=True, ax=ax4, linestyle='--')
except:
pass
plt.xlabel('Дата привлечения')
plt.title(
'Динамика удержания неплатящих пользователей на {}-й день'.format(
horizon
)
)
plt.tight_layout()
plt.show()
Проверим работу функции.
# plot_retention(retention, retention_history, horizon_days, window=7)
Функция работает корректно.
plot_conversion() — для построения графика конверсии¶
# функция для визуализации конверсии
def plot_conversion(conversion, conversion_history, horizon, window=7):
'''
Функция для визуализации конверсии.
Получает:
conversion - сгруппированные данные по конверсии
conversion_history - данные по конверсии во времени
horizon_days - количество дней lifetime, для которых выполняем анализ, int, одна неделя = 7, две = 14
window - ширина "окна" сглаживания, int, например 4.
Возвращает:
кривые конверсси
динамику конверсии
'''
# задаём размер сетки для графиков
plt.figure(figsize=(15, 5))
# исключаем размеры когорт
conversion = conversion.drop(columns=['cohort_size'])
# в таблице динамики оставляем только нужный лайфтайм
conversion_history = conversion_history.drop(columns=['cohort_size'])[
[horizon - 1]
]
# первый график — кривые конверсии
ax1 = plt.subplot(1, 2, 1)
conversion.iloc[:6].T.plot(grid=True, ax=ax1, linestyle='-')
try:
conversion.iloc[6:].T.plot(grid=True, ax=ax1, linestyle='--')
except:
pass
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('Конверсия пользователей')
# второй график — динамика конверсии
ax2 = plt.subplot(1, 2, 2, sharey=ax1)
columns = [
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
name for name in conversion_history.index.names if name not in ['dt']
]
filtered_data = conversion_history.pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
# display(filtered_data.head())
fd = filter_data(filtered_data, window)
fd.iloc[:, :6].plot(grid=True, ax=ax2, linestyle='-')
try:
fd.iloc[:, 6:].plot(grid=True, ax=ax2, linestyle='--')
except:
pass
plt.xlabel('Дата привлечения')
plt.title('Динамика конверсии пользователей на {}-й день'.format(horizon))
plt.tight_layout()
plt.show()
Проверим работу функции.
# plot_conversion(conversion, conversion_history, horizon_days, window=7)
Функция работает корректно. Стиль линий уникален для каждого графика.
plot_ltv_roi — для визуализации LTV и ROI¶
# функция для визуализации LTV и ROI
def plot_ltv_roi(ltv, ltv_history, roi, roi_history, horizon, window=7):
'''
Функция для визуализации LTV и ROI.
Получает:
ltv - таблица LTV (сгруппированная)
ltv_history - таблица динамики LTV во времени
roi - таблица ROI (сгруппированная)
roi_in_time - таблица динамики ROI во времени
horizon_days - количество дней lifetime, для которых выполняем анализ, int, одна неделя = 7, две = 14
window - ширина "окна" сглаживания, int, например 4.
Возвращает:
кривые ltv
динамика ltv
динамика cac
кривые roi
динамика roi
'''
# задаём сетку отрисовки графиков
plt.figure(figsize=(20, 10))
# из таблицы ltv исключаем размеры когорт
ltv = ltv.drop(columns=['cohort_size'])
# в таблице динамики ltv оставляем только нужный лайфтайм
ltv_history = ltv_history.drop(columns=['cohort_size'])[[horizon - 1]]
# стоимость привлечения запишем в отдельный фрейм
cac_history = roi_history[['cac']]
# из таблицы roi исключаем размеры когорт и cac
roi = roi.drop(columns=['cohort_size', 'cac'])
# в таблице динамики roi оставляем только нужный лайфтайм
roi_history = roi_history.drop(columns=['cohort_size', 'cac'])[
[horizon - 1]
]
# первый график — кривые ltv
ax1 = plt.subplot(2, 3, 1)
# Меняем стиль линий так как цветов не хватает для выделения значений
ltv.iloc[:6].T.plot(grid=True, ax=ax1, linestyle='-')
try:
ltv.iloc[6:].T.plot(grid=True, ax=ax1, linestyle='--')
except:
pass
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('LTV')
# второй график — динамика ltv
ax2 = plt.subplot(2, 3, 2, sharey=ax1)
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
columns = [name for name in ltv_history.index.names if name not in ['dt']]
filtered_data = ltv_history.pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data_roll = filter_data(filtered_data, window)
# display(filter_data_roll)
# Меняем стиль линий так как цветов не хватает для выделения значений
filter_data_roll.iloc[:, :6].plot(grid=True, ax=ax2, linestyle='-')
try:
filter_data_roll.iloc[:, 6:].plot(grid=True, ax=ax2, linestyle='--')
except:
pass
plt.xlabel('Дата привлечения')
plt.title('Динамика LTV пользователей на {}-й день'.format(horizon))
# третий график — динамика cac
ax3 = plt.subplot(2, 3, 3, sharey=ax1)
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
columns = [name for name in cac_history.index.names if name not in ['dt']]
filtered_data = cac_history.pivot_table(
index='dt', columns=columns, values='cac', aggfunc='mean'
)
filter_data_roll = filter_data(filtered_data, window)
# Меняем стиль линий так как цветов не хватает для выделения значений
filter_data_roll.iloc[:, :6].plot(grid=True, ax=ax3, linestyle='-')
try:
filter_data_roll.iloc[:, 6:].plot(grid=True, ax=ax3, linestyle='--')
except:
pass
plt.xlabel('Дата привлечения')
plt.title('Динамика стоимости привлечения пользователей')
# четвёртый график — кривые roi
ax4 = plt.subplot(2, 3, 4)
# Меняем стиль линий так как цветов не хватает для выделения значений
roi.iloc[:6].T.plot(grid=True, ax=ax4, linestyle='-')
try:
roi.iloc[6:].T.plot(grid=True, ax=ax4, linestyle='--')
except:
pass
plt.axhline(y=1, color='red', linestyle='-.', label='Уровень окупаемости')
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('ROI')
# пятый график — динамика roi
ax5 = plt.subplot(2, 3, 5, sharey=ax4)
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
columns = [name for name in roi_history.index.names if name not in ['dt']]
filtered_data = roi_history.pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data_roll = filter_data(filtered_data, window)
# Меняем стиль линий так как цветов не хватает для выделения значений
filter_data_roll.iloc[:, :6].plot(grid=True, ax=ax5, linestyle='-')
try:
filter_data_roll.iloc[:, 6:].plot(grid=True, ax=ax5, linestyle='--')
except:
pass
plt.axhline(y=1, color='red', linestyle='-.', label='Уровень окупаемости')
plt.xlabel('Дата привлечения')
plt.title('Динамика ROI пользователей на {}-й день'.format(horizon))
plt.tight_layout()
plt.show()
Проверим работу функции.
# plot_ltv_roi(ltv_channel, ltv_channel_history, roi_channel, roi_channel_history, horizon_days, window=7)
Функция работает корректно. До 12 категорий отобразит разными линиями.
Исследовательский анализ данных¶
Создадим профили пользователей¶
profiles = get_profiles(visits, orders, orders, ad_costs)
profiles
| user_id | first_ts | channel | device | region | dt | month | payer | acquisition_cost | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 599326 | 2019-05-07 20:58:57 | FaceBoom | Mac | United States | 2019-05-07 | 2019-05-01 | True | 1.088172 |
| 1 | 4919697 | 2019-07-09 12:46:07 | FaceBoom | iPhone | United States | 2019-07-09 | 2019-07-01 | False | 1.107237 |
| 2 | 6085896 | 2019-10-01 09:58:33 | organic | iPhone | France | 2019-10-01 | 2019-10-01 | False | 0.000000 |
| 3 | 22593348 | 2019-08-22 21:35:48 | AdNonSense | PC | Germany | 2019-08-22 | 2019-08-01 | False | 0.988235 |
| 4 | 31989216 | 2019-10-02 00:07:44 | YRabbit | iPhone | United States | 2019-10-02 | 2019-10-01 | False | 0.230769 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 150003 | 999956196527 | 2019-09-28 08:33:02 | TipTop | iPhone | United States | 2019-09-28 | 2019-09-01 | False | 3.500000 |
| 150004 | 999975439887 | 2019-10-21 00:35:17 | organic | PC | UK | 2019-10-21 | 2019-10-01 | False | 0.000000 |
| 150005 | 999976332130 | 2019-07-23 02:57:06 | TipTop | iPhone | United States | 2019-07-23 | 2019-07-01 | False | 2.600000 |
| 150006 | 999979924135 | 2019-09-28 21:28:09 | MediaTornado | PC | United States | 2019-09-28 | 2019-09-01 | False | 0.205714 |
| 150007 | 999999563947 | 2019-10-18 19:57:25 | organic | iPhone | United States | 2019-10-18 | 2019-10-01 | False | 0.000000 |
150008 rows × 9 columns
Всего 150 008 профилей пользователей.
Дата привлечения пользователей.
Посмотрим на даты в исходных данных.
for t, item in {'visits': visits.session_start, 'visits': visits.session_end,
'orders': orders.event_dt,
'ad_costs': ad_costs.dt}.items():
print(t)
print(item.name, '\t', item.min(), '\t', item.max(), '\n', '*'*60)
visits session_end 2019-05-01 00:07:06 2019-11-01 01:38:46 ************************************************************ orders event_dt 2019-05-01 00:28:11 2019-10-31 23:56:56 ************************************************************ ad_costs dt 2019-05-01 00:00:00 2019-10-27 00:00:00 ************************************************************
- Посещения пользователей выполнялись с 1 мая 2019 года до 1 ноября 2019 года.
- Заказы пользователей размещались с 1 мая по 31 октября 2019 года.
- Оплата рекламы выполнялась с 1 мая до 27 октября 2019 год.
Посмотрим на даты создания профилей пользователей.
print('Минимальная:', profiles['first_ts'].min().date(),
'Максимальная:', profiles['first_ts'].max().date())
Минимальная: 2019-05-01 Максимальная: 2019-10-27
Интервал создания профилей пользователей соответствует ожидаемому.
Видим, что за пять дней с 28 октября по 1 ноября данные по размещению рекламы не выгружались. Так как анализ выполняется по состоянию на 1 ноября 2019 года и предусмотренный бизнес-планом период окупаемости рекламы составляет 14 дней, то отсутствие данных по рекламе за последнюю неделю не повлияет на расчет ключевых метрик: LTV, ROI.
Так как пользователи уже привлечены и приносят деньги, не будем исключаться из рассмотрения профили не успевшие "прожить" срок окупаемости, предусмотренный бизнес-планом.
Количество пользователей и доля платящих из каждой страны¶
Распределение пользователей по странам.
ratio_region = profile_ratio(df=profiles, group='region')
disp_df(ratio_region)
| users | ratio_users | |
|---|---|---|
| region | ||
| United States | 100002 | 66.7% |
| UK | 17575 | 11.7% |
| France | 17450 | 11.6% |
| Germany | 14981 | 10.0% |
Пользователи приходят в приложение из четырех стран: США - 66.7% (100 002 человека), Великобритании - 11.7% (17 575 человек), Франции - 11.6% (17 450 человек), Германии - 10% (14 981 человек).
Распределение платящих пользователей по странам.
ratio_payer_region = profile_ratio(df=profiles, group='region', payer=True)
disp_df(ratio_payer_region)
| payers | ratio_payers | |
|---|---|---|
| region | ||
| United States | 6902 | 77.7% |
| UK | 700 | 7.9% |
| France | 663 | 7.5% |
| Germany | 616 | 6.9% |
Платящие пользователи приходят в приложение в 77.7% случаев из США 6902 человека. Великобритания дает 7.9% платящих пользователей 700 человек, Франция - 7.5% 663 человека, Германия - 6.9% 616 человек.
Выведем информацию на графиках.
# Зададим стиль графиков
try:
plt.style.use('seaborn-v0_8-pastel') # Мягкие оттенки - стиль
except:
plt.style.use('seaborn-pastel') # Мягкие оттенки - стиль
plt.figure(figsize=(13, 5))
# первый график — Распределение пользователей по странам
ax1 = plt.subplot(1, 2, 1)
ratio_region['ratio_users'].plot(kind='pie', labels=ratio_region.index, autopct='%1.1f%%',
legend=False, title='Доля стран среди пользователей')
# второй график - распределение платящих пользователей по странам.
ax2 = plt.subplot(1, 2, 2)
ratio_payer_region['ratio_payers'].plot(kind='pie', labels=ratio_payer_region.index, autopct='%1.1f%%',
legend=False, title='Доля стран среди платящих пользователей')
plt.tight_layout()
plt.show()
Большую часть платящих пользователей дает США - 77.7%.
Отобразим конверсию уникальных пользователей в платящих пользователей по каждой стране.
result = profile_cr(df=profiles, group='region')
disp_df(result)
| users | payers | cr | |
|---|---|---|---|
| region | |||
| United States | 100002 | 6902 | 6.9% |
| Germany | 14981 | 616 | 4.1% |
| UK | 17575 | 700 | 4.0% |
| France | 17450 | 663 | 3.8% |
Конверсия обычных пользователей в платящих в США составляет 6.9%, в Великобритании 4%, Франции 3.8%, Германии 4.1%.
Выведем результат на графике.
# Третий график - соотношение платящих и неплатящих пользователей внутри страны.
width = 0.3
x = np.arange(len(result))
ax3 = plt.subplot(1, 1, 1)
rects1 = ax3.bar(x - width/2, result['payers'], width, label='payer')
rects2 = ax3.bar(x + width/2, result['users'], width, label='users')
ax3.set_title('Соотношение платящих пользователей и всех пользователей по странам.')
ax3.set_xticks(x)
ax3.set_xticklabels(result.index)
ax3.legend()
plt.tight_layout()
plt.show()
Вывод. Количество пользователей и доля платящих из каждой страны¶
Пользователи приходят в приложение из четырех стран: США - 66.7% (100 002 человека), Великобритании - 11.7% (17 575 человек), Франции - 11.6% (17 450 человек), Германии - 10% (14 981 человек).
Больше всего платящих пользователей приходится на США 6902 человека или 77.7%. Великобритания дает 7.9% платящих пользователей 700 человек, Франции - 7.5% 663 человека, Германии - 6.9% 616 человек.
Конверсия обычных пользователей в платящих в США составляет 6.9%, в Великобритании 4%, Франции 3.8%, Германии 4.1%.
Предпочитаемые пользователями устройства¶
disp_df(profile_ratio(df=profiles, group='device', payer=False))
| users | ratio_users | |
|---|---|---|
| device | ||
| iPhone | 54479 | 36.3% |
| Android | 35032 | 23.4% |
| PC | 30455 | 20.3% |
| Mac | 30042 | 20.0% |
Клиенты пользуются устройствами iPhone 36.3%, Android 23.4%, PC 20.3%, Mac 20%.
Предпочтения платящих пользователей.
disp_df(profile_ratio(df=profiles, group='device', payer=True))
| payers | ratio_payers | |
|---|---|---|
| device | ||
| iPhone | 3382 | 38.1% |
| Android | 2050 | 23.1% |
| Mac | 1912 | 21.5% |
| PC | 1537 | 17.3% |
Платящие клиенты предпочитают iPhone 38.1%, Android 23.1%, Mac 21.5%, PC 17.3%. Мобильных клиентов 61.2%. Значит особое внимание нужно уделять работе приложения на мобильных устройствах.
Конверсия пользователей в платящих пользователей по типам устройств.
disp_df(profile_cr(df=profiles, group='device'))
| users | payers | cr | |
|---|---|---|---|
| device | |||
| Mac | 30042 | 1912 | 6.4% |
| iPhone | 54479 | 3382 | 6.2% |
| Android | 35032 | 2050 | 5.9% |
| PC | 30455 | 1537 | 5.0% |
Вывод Предпочитаемые пользователями устройства¶
Клиенты пользуются устройствами iPhone 36.3%, Android 23.4%, PC 20.3%, Mac 20%.
Платящие клиенты предпочитают iPhone 38.1%, Android 23.1%, Mac 21.5%, PC 17.3%. Мобильных клиентов 61.2%. Значит, особое внимание нужно уделять работе приложения на мобильных устройствах.
Конверсия пользователей в платящих пользователей составляет: Mac 6.4%, iPhone 6.2%, Android 5.9%, PC 5%.
Рекламные источники привлечения и каналы, из которых пришло больше всего платящих пользователей¶
Всего каналов привлечения.
profiles.groupby('channel').agg({'user_id': 'nunique'}).count()
user_id 11 dtype: int64
Распределение пользователей по каналам привлечения.
# Всего пользователей по каналам привлечения
channel_users = profiles.groupby('channel').agg({'user_id': 'nunique'})
# Количество уникальных платящих пользователей по каналам привлечения
channel_payer_users = profiles.query('payer == True').groupby('channel').agg({'user_id': 'nunique'})
# Объединяем результат
channel_users = channel_users.merge(channel_payer_users, on='channel', how = 'left')
# Уточняем названия столбцов
channel_users.columns = ['users', 'payers']
# Процент пользователей по данному каналу от всего пользователей
channel_users['ratio_users'] = channel_users['users'] / channel_users['users'].sum()
# Процент платящих пользователей от всего платящих пользователей по этому каналу.
channel_users['ratio_payers'] = channel_users['payers'] / channel_users['payers'].sum()
# Процент платящих пользователей от всего пользователей по этому каналу.
channel_users['ratio_payers_from_users'] = channel_users['payers'] / channel_users['users'].sum()
# Конверсия всех пользователей в платящих по этому каналу.
channel_users['cr_payers'] = channel_users['ratio_payers_from_users'] / channel_users['ratio_users']
# Сортируем и форматируем результаты
channel_users.sort_values(by='ratio_payers_from_users', ascending=False, inplace=True)
channel_users.style.format({'ratio_users': '{:.1%}',
'ratio_payers': '{:.1%}',
'ratio_payers_from_users': '{:.1%}',
'cr_payers': '{:.1%}'
})
| users | payers | ratio_users | ratio_payers | ratio_payers_from_users | cr_payers | |
|---|---|---|---|---|---|---|
| channel | ||||||
| FaceBoom | 29144 | 3557 | 19.4% | 40.1% | 2.4% | 12.2% |
| TipTop | 19561 | 1878 | 13.0% | 21.1% | 1.3% | 9.6% |
| organic | 56439 | 1160 | 37.6% | 13.1% | 0.8% | 2.1% |
| WahooNetBanner | 8553 | 453 | 5.7% | 5.1% | 0.3% | 5.3% |
| AdNonSense | 3880 | 440 | 2.6% | 5.0% | 0.3% | 11.3% |
| RocketSuperAds | 4448 | 352 | 3.0% | 4.0% | 0.2% | 7.9% |
| LeapBob | 8553 | 262 | 5.7% | 3.0% | 0.2% | 3.1% |
| OppleCreativeMedia | 8605 | 233 | 5.7% | 2.6% | 0.2% | 2.7% |
| lambdaMediaAds | 2149 | 225 | 1.4% | 2.5% | 0.1% | 10.5% |
| YRabbit | 4312 | 165 | 2.9% | 1.9% | 0.1% | 3.8% |
| MediaTornado | 4364 | 156 | 2.9% | 1.8% | 0.1% | 3.6% |
Здесь:
ratio_users- доля пользователей по каналу продвижения от общего числа пользователей;ratio_payers- доля платящих пользователей по каналу продвижения от общего числа платящих пользователей;ratio_payers_from_users- доля платящих пользователей по каналу продвижения от общего числа всех пользователей;cr_payers- конверсия пользователей в платящих пользователей по каналу продвижения.
Процент привлеченных пользователей из каналов кроме первой тройки.
round(channel_users.iloc[3:]['users'].sum() / channel_users['users'].sum() * 100, 1)
29.9
Вывод. Рекламные источники привлечения и каналы, из которых пришло больше всего платящих пользователей¶
Всего используется 11 каналов привлечения.
Больше всего платящих пользователей пришло из каналов FaceBoom 3557 человек или 40.1% от общего числа платящих пользователей, TipTop 1878 человек (21.1%), organic 1160 (13.3%). Канал AdNonSense дал 440 человек (5%), WahooNetBanner 453 человека (5.1%). Прочие каналы дали от 156 до 262 человек каждый или 1.8 - 3% платящих пользователей. Каналы кроме TipTop, FaceBoom, organic дали 29.9% пользователей.
В процентах от всех пользователей доля платящих составляет: FaceBoom 2.4%, TipTop 1.3%, organic 0.8%, AdNonSense 0.3%, WahooNetBanner 0.3%. Прочие каналы дали 0.1 - 0.2%.
Наилучшие коэффициенты конверсии привлеченных пользователей в платящих видим в каналах FaceBoom 12.4%, AdNonSense 11.5%, TipTop 10%. Чуть хуже в lambdaMediaAds 7.1%, RocketSuperAds 6.7%, WahooNetBanner 5.3%.
Спорные пользователи канала organic¶
Значительное количество пользователей пришло из канала organic - 37.6%. Проверим есть ли по этим пользователям платные закупки.
profiles.loc[profiles['channel'] == 'organic'].head()
| user_id | first_ts | channel | device | region | dt | month | payer | acquisition_cost | |
|---|---|---|---|---|---|---|---|---|---|
| 2 | 6085896 | 2019-10-01 09:58:33 | organic | iPhone | France | 2019-10-01 | 2019-10-01 | False | 0.0 |
| 5 | 34186288 | 2019-05-09 17:29:23 | organic | Android | United States | 2019-05-09 | 2019-05-01 | False | 0.0 |
| 6 | 38448301 | 2019-08-16 11:56:00 | organic | Android | United States | 2019-08-16 | 2019-08-01 | False | 0.0 |
| 10 | 62222884 | 2019-06-19 15:14:57 | organic | iPhone | Germany | 2019-06-19 | 2019-06-01 | False | 0.0 |
| 11 | 64614231 | 2019-10-22 09:51:19 | organic | Android | UK | 2019-10-22 | 2019-10-01 | False | 0.0 |
visits.head(3)
| user_id | region | device | channel | session_start | session_end | |
|---|---|---|---|---|---|---|
| 0 | 981449118918 | United States | iPhone | organic | 2019-05-01 02:36:01 | 2019-05-01 02:45:01 |
| 1 | 278965908054 | United States | iPhone | organic | 2019-05-01 04:46:31 | 2019-05-01 04:47:35 |
| 2 | 590706206550 | United States | Mac | organic | 2019-05-01 14:09:25 | 2019-05-01 15:32:08 |
Идентификаторы пользователей, пришедших из канала organic.
organic_id = profiles.loc[profiles['channel'] == 'organic']['user_id']
organic_id.head(3)
2 6085896 5 34186288 6 38448301 Name: user_id, dtype: int64
Количество пользователей, которые отнесены к каналу organic и которые имеют записи о визитах с платных рекламных каналов.
debatable_users = visits.query('user_id in @organic_id and channel != "organic"').pivot_table(
index='channel', values='user_id', aggfunc='nunique').sum()
debatable_users
user_id 1572 dtype: int64
Процент спорных пользователей от общего количества пользователей organic.
round(debatable_users / 56439 * 100, 1)
user_id 2.8 dtype: float64
Посмотрим на некоторых пользователей.
# id пользователей, отнесенных к каналу organic и имеющих посещения с других спорных каналов.
debatable_id = visits.query('user_id in @organic_id and channel != "organic"')['user_id']
visits.query('user_id in @debatable_id').sort_values(by=['user_id', 'session_start']).head(10)
| user_id | region | device | channel | session_start | session_end | |
|---|---|---|---|---|---|---|
| 170339 | 201829675 | United States | PC | organic | 2019-10-02 06:22:55 | 2019-10-02 06:55:08 |
| 171393 | 201829675 | United States | PC | FaceBoom | 2019-10-03 14:41:33 | 2019-10-03 14:44:03 |
| 174258 | 201829675 | United States | PC | FaceBoom | 2019-10-05 18:04:05 | 2019-10-05 18:21:27 |
| 36690 | 367679392 | United States | Mac | organic | 2019-06-07 06:34:18 | 2019-06-07 06:53:56 |
| 43875 | 367679392 | United States | Mac | MediaTornado | 2019-06-16 18:37:50 | 2019-06-16 19:05:12 |
| 47819 | 397006821 | United States | Mac | organic | 2019-06-20 01:04:09 | 2019-06-20 01:35:27 |
| 48643 | 397006821 | United States | Mac | MediaTornado | 2019-06-21 23:02:50 | 2019-06-21 23:04:25 |
| 244070 | 1101776810 | Germany | Android | organic | 2019-07-12 11:40:41 | 2019-07-12 11:55:18 |
| 244402 | 1101776810 | Germany | Android | OppleCreativeMedia | 2019-07-13 07:43:24 | 2019-07-13 08:02:39 |
| 245316 | 1101776810 | Germany | Android | organic | 2019-07-15 14:24:58 | 2019-07-15 15:25:57 |
Проверим, есть ли пользователи у которых несколько платных каналов привлечения.
visits.query('user_id in @debatable_id and channel != "organic"').pivot_table(index='user_id', columns='channel',
values='session_start', aggfunc='count').T.count().sort_values(ascending=False).head(10)
user_id 66261710286 4 508067661069 3 597661910247 3 842052040339 3 971284236917 3 388851901664 3 445230891031 3 896941673419 3 895988402104 3 791612896135 3 dtype: int64
Такие пользователи есть. Посмотрим на одного из них.
visits.query('user_id == 66261710286').sort_values(by='session_start').head(15)
| user_id | region | device | channel | session_start | session_end | |
|---|---|---|---|---|---|---|
| 51161 | 66261710286 | United States | Android | organic | 2019-06-23 21:36:16 | 2019-06-23 21:53:36 |
| 52061 | 66261710286 | United States | Android | FaceBoom | 2019-06-24 21:11:00 | 2019-06-24 21:46:21 |
| 53154 | 66261710286 | United States | Android | RocketSuperAds | 2019-06-25 15:47:01 | 2019-06-25 16:31:43 |
| 55427 | 66261710286 | United States | Android | RocketSuperAds | 2019-06-27 08:19:54 | 2019-06-27 11:04:44 |
| 56669 | 66261710286 | United States | Android | RocketSuperAds | 2019-06-28 05:30:41 | 2019-06-28 05:30:57 |
| 57911 | 66261710286 | United States | Android | RocketSuperAds | 2019-06-29 03:24:58 | 2019-06-29 03:58:40 |
| 63070 | 66261710286 | United States | Android | TipTop | 2019-07-03 20:16:56 | 2019-07-03 20:40:50 |
| 66689 | 66261710286 | United States | Android | RocketSuperAds | 2019-07-06 12:42:50 | 2019-07-06 12:54:05 |
| 71260 | 66261710286 | United States | Android | RocketSuperAds | 2019-07-10 04:33:01 | 2019-07-10 05:00:22 |
| 74277 | 66261710286 | United States | Android | RocketSuperAds | 2019-07-13 18:34:34 | 2019-07-13 18:43:09 |
| 84291 | 66261710286 | United States | Android | RocketSuperAds | 2019-07-24 01:53:02 | 2019-07-24 02:07:12 |
| 86020 | 66261710286 | United States | Android | RocketSuperAds | 2019-07-26 12:17:00 | 2019-07-26 12:46:24 |
| 94843 | 66261710286 | United States | Android | RocketSuperAds | 2019-08-03 09:03:49 | 2019-08-03 09:05:32 |
| 115146 | 66261710286 | United States | Android | RocketSuperAds | 2019-08-20 05:05:54 | 2019-08-20 05:24:30 |
| 116307 | 66261710286 | United States | Android | RocketSuperAds | 2019-08-21 19:50:13 | 2019-08-21 20:03:14 |
Не видим прямой закономерности, к какому платному каналу отнести пользователя.
Посмотрим на оплаты такого пользователя.
orders.query('user_id == 66261710286').sort_values(by='event_dt').head(15).style.format(formatter='$ {:.2f}', subset='revenue')
| user_id | event_dt | revenue | |
|---|---|---|---|
| 4840 | 66261710286 | 2019-06-25 15:47:35 | $ 4.99 |
| 6017 | 66261710286 | 2019-07-03 20:17:11 | $ 4.99 |
| 9194 | 66261710286 | 2019-07-24 01:53:15 | $ 4.99 |
| 13610 | 66261710286 | 2019-08-21 19:50:20 | $ 5.99 |
| 13924 | 66261710286 | 2019-08-23 00:20:13 | $ 4.99 |
| 14086 | 66261710286 | 2019-08-24 21:16:08 | $ 4.99 |
| 15512 | 66261710286 | 2019-09-01 01:09:33 | $ 4.99 |
| 17727 | 66261710286 | 2019-09-13 23:17:57 | $ 4.99 |
| 18936 | 66261710286 | 2019-09-20 07:53:09 | $ 4.99 |
| 24326 | 66261710286 | 2019-10-15 11:39:02 | $ 4.99 |
| 26569 | 66261710286 | 2019-10-25 13:37:19 | $ 4.99 |
Оплаты не являются ритмичными по датам. Закономерность не прослеживается.
Общее количество платящих пользователей отнесенных к каналу organic.
profiles.loc[(profiles['payer']) & (profiles['channel'] == 'organic')]['user_id'].count()
1160
Общее количество платящих пользователей, являющихся спорными по атрибуции.
profiles.query('user_id in @debatable_id and payer')['user_id'].count()
384
Процент спорных платящих пользователей, отнесенных к каналу organic, от общего числа платящих пользователей, отнесенныех к каналу organic.
round(profiles.query('user_id in @debatable_id and payer')['user_id'].count() / \
profiles.loc[(profiles['payer']) & (profiles['channel'] == 'organic')]['user_id'].count() * 100, 1)
33.1
Общий процент платящих пользователей, отнесенных к каналу organic.
round(profiles.query('channel == "organic"')['payer'].mean() * 100, 1)
2.1
Вывод Спорные пользователи канала organic¶
Видим значительное количество пересечений входов пользователей с бесплатных каналов привлечения и с платных. Это достаточно обычная ситуация. Пользователи входят то по рекламным объявлениям, то через прямой поиск.
Оставляем принятую по умолчанию стратегию присвоения пользователя каналу - по первой сессии.
При этом среди платящих пользователей, отнесенных к каналу organic, 56.9% имеют спорную атрибуцию: имеют визиты как с канала organic, так и с платных каналов продвижения.
Изучение влияния спорных сессий пользователей на окупаемость рекламы можно провести отдельно.
Маркетинг¶
Общая сумма расходов на маркетинг в далларах.
round(ad_costs['costs'].sum(), 2)
105497.3
Распределение бюджета по рекламным источникам¶
# Затраты по каналам привлечения
costs_ratio = ad_costs.groupby('channel').agg({'costs': 'sum'}).sort_values(by='costs', ascending=False)
# Доля канала в общих затратах на рекламу
costs_ratio['ratio_costs'] = costs_ratio['costs'] / costs_ratio['costs'].sum()
# Добавим информацию по платящим пользователям
costs_ratio = costs_ratio.merge(channel_users, on='channel', how='left')
# Доля канала среди платящих пользователей
costs_ratio['cr_costs_payers'] = costs_ratio['ratio_payers_from_users'] / costs_ratio['ratio_costs']
# costs_ratio
# Выведем на экран
costs_ratio[['costs', 'ratio_costs', 'ratio_payers_from_users', 'cr_costs_payers']].style.format({
'costs': '${:.2f}',
'ratio_costs': '{:.1%}',
'ratio_payers_from_users': '{:.1%}',
'cr_costs_payers': '{:.1%}'
})
| costs | ratio_costs | ratio_payers_from_users | cr_costs_payers | |
|---|---|---|---|---|
| channel | ||||
| TipTop | $54751.30 | 51.9% | 1.3% | 2.4% |
| FaceBoom | $32445.60 | 30.8% | 2.4% | 7.7% |
| WahooNetBanner | $5151.00 | 4.9% | 0.3% | 6.2% |
| AdNonSense | $3911.25 | 3.7% | 0.3% | 7.9% |
| OppleCreativeMedia | $2151.25 | 2.0% | 0.2% | 7.6% |
| RocketSuperAds | $1833.00 | 1.7% | 0.2% | 13.5% |
| LeapBob | $1797.60 | 1.7% | 0.2% | 10.3% |
| lambdaMediaAds | $1557.60 | 1.5% | 0.1% | 10.2% |
| MediaTornado | $954.48 | 0.9% | 0.1% | 11.5% |
| YRabbit | $944.22 | 0.9% | 0.1% | 12.3% |
Самый большой бюджет у канала TipTop 54 751.30 доллара или 51.9%. При этом в платящих пользователи этого канала конвертируются хуже всего (2.4%).
Канал FaceBoom дает 30.8% бюджета (32 445.60 доллара), конверсия в платящих 7.7%. WahooNetBanner 4.9% (5 151.00 доллара) cr 6.2%, AdNonSense 3.7% (3 911.25 доллара) cr 7.9%.
Прочие каналы дают в бюджет менее 2% с конверсией в платящих от 7.6% до 13.5%.
Посчитаем медиану конвертации бюджета на маркетинг в платящих пользователей.
cr_costs_payer_median = costs_ratio['cr_costs_payers'].median()
round(cr_costs_payer_median * 100, 2)
9.04
Выберем каналы с коэффициентом конверсии пользователей в платящих пользователей выше медианы.
(costs_ratio.loc[costs_ratio['cr_costs_payers'] > cr_costs_payer_median,
['costs', 'ratio_costs', 'ratio_payers_from_users', 'cr_costs_payers']]
.style.format({'costs': '${:.2f}', 'ratio_costs': '{:.1%}',
'ratio_payers_from_users': '{:.1%}', 'cr_costs_payers': '{:.1%}'})
)
| costs | ratio_costs | ratio_payers_from_users | cr_costs_payers | |
|---|---|---|---|---|
| channel | ||||
| RocketSuperAds | $1833.00 | 1.7% | 0.2% | 13.5% |
| LeapBob | $1797.60 | 1.7% | 0.2% | 10.3% |
| lambdaMediaAds | $1557.60 | 1.5% | 0.1% | 10.2% |
| MediaTornado | $954.48 | 0.9% | 0.1% | 11.5% |
| YRabbit | $944.22 | 0.9% | 0.1% | 12.3% |
Предварительно целесообразно увеличить финансирование каналов продвижения: OppleCreativeMedia, RocketSuperAds, LeapBob, MediaTornado, YRabbit так как привлеченные с них пользователи лучше всего конвертируются в платящих (от 10% до 11.8%).
Зададим стиль линий графика.
# plt.style.available
try:
plt.style.use('seaborn-bright')
plt.style.use('seaborn-whitegrid')
except:
plt.style.use('seaborn-v0_8-bright')
plt.style.use('seaborn-v0_8-whitegrid')
График с визуализацией динамики изменения расходов по неделям по всем источникам и график динамики изменения расходов по месяцам суммарно по всем источникам привлечения клиентов.
plt.figure(figsize=(15,5))
# Первый график - динамика расходов по неделям.
ax1 = plt.subplot(1, 2, 1)
result = ad_costs.pivot_table(index='week', values='costs', aggfunc='sum')
# display(result.head())
result.plot(ax=ax1, title='График изменения расходов на реклау во времени, недели',
xlabel='Неделя', ylabel='Расходы', grid=True, linestyle='-');
# Второй график - динамика расходов по месяцам.
ax2 = plt.subplot(1, 2, 2)
result = ad_costs.pivot_table(index='month', values='costs', aggfunc='sum')
# display(result.head())
result.plot(ax=ax2, title='График изменения расходов на реклау во времени, месяца',
xlabel='Месяц', ylabel='Расходы', grid=True, linestyle='-');
plt.show()
Расходы на рекламу росли месяц к месяцу с мая по сентябрь от 10 тыс. долларов до 220 тыс. долларов, затем стали снижаться и достигли 210 тыс. долларов в октябре.
Внутри месяца расходы на рекламу распределены неравномерно.
График с визуализацией динамики изменения расходов по неделям по каждому источнику и график динамики изменения расходов по месяцам по каждому источнику.
Посмотрим на динамику расходов на рекламу по каждому платному источнику привлечения пользователей.
plt.figure(figsize=(15,5))
# Первый график - динамика расходов по неделям.
ax1 = plt.subplot(1, 2, 1)
result = ad_costs.pivot_table(index='week', columns='channel', values='costs', aggfunc='sum')
# display(result.head())
result.iloc[:, :6].plot(ax=ax1, title='График изменения расходов на реклау во времени, недели',
xlabel='Неделя', ylabel='Расходы', grid=True, linestyle='-');
result.iloc[:, 6:].plot(ax=ax1, title='График изменения расходов на реклау во времени, недели',
xlabel='Неделя', ylabel='Расходы', grid=True, linestyle='--');
# Второй график - динамика расходов по месяцам.
ax2 = plt.subplot(1, 2, 2)
result = ad_costs.pivot_table(index='month', columns='channel', values='costs', aggfunc='sum')
# display(result.head())
result.iloc[:, :6].plot(ax=ax2, title='График изменения расходов на реклау во времени, месяца',
xlabel='Месяц', ylabel='Расходы', grid=True, linestyle='-');
result.iloc[:, 6:].plot(ax=ax2, title='График изменения расходов на реклау во времени, месяца',
xlabel='Месяц', ylabel='Расходы', grid=True, linestyle='--');
plt.show()
Весь рост расходов на рекламу дали два канала привлечения пользователей: TipTop и FaceBoom. В прочих каналах расходы на рекламу меняются незначительно.
Вывод. Распределение бюджета по рекламным источникам¶
Общая сумма расходов на маркетинг $ 105 497.30
Самый большой бюджет у канала TipTop 54 751.30 доллара или 51.9%. При этом в платящих пользователи этого канала конвертируются хуже всего (2.4%).
Канал FaceBoom дает 30.8% бюджета (32 445.60 доллара), конверсия в платящих 7.7%. WahooNetBanner 4.9% (5 151.00 доллара) cr 6.2%, AdNonSense 3.7% (3 911.25 доллара) cr 7.9%.
Прочие каналы дают в бюджет менее 2% с конверсией в платящих от 7.6% до 13.5%.
Предварительно целесообразно увеличить финансирование каналов продвижения: OppleCreativeMedia, RocketSuperAds, LeapBob, MediaTornado, YRabbit так как привлеченные с них пользователи лучше всего конвертируются в платящих (от 10% до 11.8%).
Расходы на рекламу росли месяц к месяцу с мая по сентябрь от 10 тыс. долларов до 220 тыс. долларов, затем стали снижаться и достигли 210 тыс. долларов в октябре.
Весь рост расходов на рекламу дали два канала привлечения пользователей: TipTop и FaceBoom. В прочих каналах расходы на рекламу меняются незначительно.
Внутри месяца расходы на рекламу распределены неравномерно.
Средняя стоимость привлечения одного пользователя из каждого источника рекламы¶
# Средняя стоимость привлечения пользователя по каналам
acquisition_cost_mean = round(profiles.groupby('channel').agg({'acquisition_cost': 'mean'}).sort_values(
by='acquisition_cost', ascending=False), 2)
# Переименуем колонки
acquisition_cost_mean.columns = ['acquisition_cost_user']
acquisition_cost_mean['k_next'] = round(acquisition_cost_mean['acquisition_cost_user'].shift(1).div(
acquisition_cost_mean['acquisition_cost_user'], axis=0), 1)
acquisition_cost_mean
| acquisition_cost_user | k_next | |
|---|---|---|
| channel | ||
| TipTop | 2.80 | NaN |
| FaceBoom | 1.11 | 2.5 |
| AdNonSense | 1.01 | 1.1 |
| lambdaMediaAds | 0.72 | 1.4 |
| WahooNetBanner | 0.60 | 1.2 |
| RocketSuperAds | 0.41 | 1.5 |
| OppleCreativeMedia | 0.25 | 1.6 |
| YRabbit | 0.22 | 1.1 |
| MediaTornado | 0.22 | 1.0 |
| LeapBob | 0.21 | 1.0 |
| organic | 0.00 | inf |
Построим график.
plt.barh(acquisition_cost_mean.index, acquisition_cost_mean['acquisition_cost_user']);
plt.title('Стоимость привлечения пользователя по каналам');
plt.show()
В среднем, привлечение одного пользователя из канала TipTop в 2.5 раза дороже пользователей FaceBoom и в 2.75 раза дороже пользователей AdNonSense. При этом конверсия в платящих в канале TipTop в три раза хуже, чем в FaceBoom.
Средняя цена привлечения пользователя по каналам (долларов за человека): TipTop 2.8, FaceBoom 1.11, AdNonSense 1.01, lambdaMediaAds 0.72, WahooNetBanner 0.60. В прочих каналах средняя цена привлечения ниже 0.41 доллара за человека.
Посчитаем среднюю стоимость платящего пользователя по каналам.
# Объединим с выборкой по количеству пользователй и платящих пользователей
acquisition_cost_mean = acquisition_cost_mean.merge(channel_users, on='channel')
# Посчитаем среднюю стоимость платящего пользователя по каналам
acquisition_cost_mean['acquisition_cost_payer'] = round(acquisition_cost_mean['acquisition_cost_user'] * \
acquisition_cost_mean['users'] / acquisition_cost_mean['payers'], 2)
# Отсортируем в порядке убывания средней стоимости платящего пользователя
(acquisition_cost_mean[['acquisition_cost_user', 'acquisition_cost_payer']]
.sort_values(by='acquisition_cost_payer',ascending=False)
)
| acquisition_cost_user | acquisition_cost_payer | |
|---|---|---|
| channel | ||
| TipTop | 2.80 | 29.16 |
| WahooNetBanner | 0.60 | 11.33 |
| OppleCreativeMedia | 0.25 | 9.23 |
| FaceBoom | 1.11 | 9.09 |
| AdNonSense | 1.01 | 8.91 |
| lambdaMediaAds | 0.72 | 6.88 |
| LeapBob | 0.21 | 6.86 |
| MediaTornado | 0.22 | 6.15 |
| YRabbit | 0.22 | 5.75 |
| RocketSuperAds | 0.41 | 5.18 |
| organic | 0.00 | 0.00 |
Вывод. Средняя стоимость привлечения одного пользователя из каждого источника рекламы¶
В среднем, привлечение одного пользователя из канала TipTop в 2.5 раза дороже пользователей FaceBoom и в 2.75 раза дороже пользователей AdNonSense. При этом конверсия в платящих в канале TipTop в три раза хуже, чем в FaceBoom.
Средняя цена привлечения пользователя по каналам (долларов за человека): TipTop 2.8, FaceBoom 1.11, AdNonSense 1.01, lambdaMediaAds 0.72, WahooNetBanner 0.60. В прочих каналах средняя цена привлечения пользователя ниже 0.41 доллара за человека.
С учетом конверсии обычных пользователей в платящих самые дорогие платящие пользователи в канале TipTop в среднем 29.16 доллара за человека. В два с половиной раза дешевле в канале WahooNetBanner - 11.33. Средняя стоимость в OppleCreativeMedia 9.23, FaceBoom 9.09, AdNonSense 8.91. В прочих каналах стоимость ниже 7 долларов.
Окупаемость рекламы¶
Выполним расчет окупаемости рекламы. За дату составления отчета примем 1 ноября 2019 года.
observation_date = pd.to_datetime('2019-11-01')
observation_date
Timestamp('2019-11-01 00:00:00')
В бизнес-плане заложено, что пользователи должны окупаться не позднее чем через две недели после привлечения. Укажем горизонт анализа.
horizon_days = 14
Органические пользователи приходят к нам без инвестиций в рекламу, окупаемость мгновенная. При этом канал organic дает 13.3% всех платящих пользователей. Исключим этих пользователей из анализа окупаемости рекламы.
profiles_ad = profiles.query('channel != "organic"')
profiles_ad['channel'].value_counts()
channel FaceBoom 29144 TipTop 19561 OppleCreativeMedia 8605 LeapBob 8553 WahooNetBanner 8553 RocketSuperAds 4448 MediaTornado 4364 YRabbit 4312 AdNonSense 3880 lambdaMediaAds 2149 Name: count, dtype: int64
Окупаемость рекламы¶
Проанализируем окупаемость рекламы c помощью графиков LTV и ROI, а также графики динамики LTV, CAC и ROI.
Произведем вычисления LTV и ROI.
ltv_raw, ltv, ltv_history, roi, roi_history = get_ltv(
profiles_ad, orders, observation_date, horizon_days)
print('ltv')
display(ltv, horizon_days)
print('roi')
disp(roi, horizon_days)
ltv
| cohort_size | 0.0 | 1.0 | 2.0 | 3.0 | 4.0 | 5.0 | 6.0 | 7.0 | 8.0 | 9.0 | 10.0 | 11.0 | 12.0 | 13.0 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| cohort | |||||||||||||||
| All users | 88056 | 0.34561 | 0.435864 | 0.504509 | 0.561017 | 0.611529 | 0.654408 | 0.694389 | 0.730038 | 0.764772 | 0.796861 | 0.826319 | 0.852183 | 0.878063 | 0.902137 |
14
roi
| cohort_size | cac | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| cohort | ||||||||||||||||
| All users | 88056 | $ 1.12 | 31.0% | 39.0% | 45.2% | 50.3% | 54.8% | 58.6% | 62.2% | 65.4% | 68.5% | 71.4% | 74.0% | 76.3% | 78.7% | 80.8% |
Построим графики LTV и ROI.
plot_ltv_roi(ltv, ltv_history, roi, roi_history, horizon_days, window=7)
Вывод. Окупаемость рекламы¶
В среднем выручка от пользователя достигает 0.9 доллара на 14-й день, колебание по месяцам есть, но небольшое от 0.8 до 1.1 доллара.
Стоимость привлечения пользователей в первой половине мая составила 0.7 доллара и начала расти со средины мая, достигнув 1.3 доллара в октябре.
В целом в заданные бизнес-планом сроки реклама не окупается. Реклама окупалась в мае 2019 года и в середине июня. В остальные промежутки времени окупаемости рекламы в четырнадцати дневный срок не наблюдается.
При стабильном уровне выручки от клиента расходы на рекламу растут за счет двух каналов привлечения пользователей TipTop и FaceBoom. Это может говорить о росте конкуренции на канале или о росте рекламного бюджета по каналу.
Конверсия пользователей и динамика её изменения¶
Посчитаем конверсию пользователей и выведем график.
conversion_raw, conversion, conversion_history = get_conversion(profiles_ad, orders,
observation_date, horizon_days)
disp(conversion, horizon_days)
| cohort_size | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| cohort | |||||||||||||||
| All users | 88056 | 5.9% | 6.6% | 7.0% | 7.3% | 7.4% | 7.6% | 7.7% | 7.8% | 7.8% | 7.9% | 7.9% | 8.0% | 8.0% | 8.0% |
plot_conversion(conversion, conversion_history, horizon_days, window=7)
Вывод. Конверсия пользователей и динамика её изменения¶
Вывод. Конверсия пользователей и динамика её изменения¶
Наибольшая скорость конверсии пользователей в платящих клиентов наблюдается до третьего дня, далее конверсия замедляется. В среднем пользователи конвертируются в 8% случаев.
С июня по август конверсия понижалась в конце месяца до 7%, затем восстанавливалась. Пик конверсии наблюдался в середине июня - 9.4%.
Конверсия пользователей по устройствам¶
Вычислим конверсию по устройствам.
conversion_device_raw, conversion_device, conversion_device_history = get_conversion(
profiles_ad, orders, observation_date, horizon_days, dimensions=['device'])
disp(conversion_device, horizon_days)
| cohort_size | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| device | |||||||||||||||
| Mac | 17656 | 6.4% | 7.3% | 7.7% | 7.9% | 8.1% | 8.2% | 8.3% | 8.4% | 8.5% | 8.6% | 8.6% | 8.7% | 8.7% | 8.7% |
| iPhone | 31848 | 6.3% | 7.0% | 7.4% | 7.7% | 7.9% | 8.0% | 8.1% | 8.2% | 8.3% | 8.3% | 8.4% | 8.4% | 8.5% | 8.5% |
| Android | 20546 | 5.8% | 6.6% | 7.0% | 7.2% | 7.4% | 7.6% | 7.7% | 7.7% | 7.8% | 7.9% | 7.9% | 7.9% | 8.0% | 8.0% |
| PC | 18006 | 4.7% | 5.3% | 5.6% | 5.8% | 6.0% | 6.2% | 6.3% | 6.4% | 6.4% | 6.5% | 6.5% | 6.5% | 6.5% | 6.5% |
Построим графики.
plot_conversion(conversion_device, conversion_device_history, horizon_days, window=7)
Вывод. Конверсия пользователей по устройствам¶
Конверсия пользователей по устройствам: Mac 8.7%, iPhone 8.5%, Android 8%, PC 6.5%.
Конверсия 14-го дня по устройствам колеблется от даты к дате в пределах 3 - 5%, что говорит о высокой конкуренции. Сильнее всего меняется конверсия на устройствах PC.
Конверсия пользователей по регионам¶
Вычислим конверсию по регионам.
conversion_region_raw, conversion_region, conversion_region_history = get_conversion(
profiles_ad, orders, observation_date, horizon_days, dimensions=['region'])
disp(conversion_region, horizon_days)
| cohort_size | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| region | |||||||||||||||
| United States | 58166 | 7.2% | 8.1% | 8.5% | 8.7% | 9.0% | 9.1% | 9.2% | 9.3% | 9.4% | 9.5% | 9.5% | 9.5% | 9.6% | 9.6% |
| Germany | 8969 | 3.2% | 3.9% | 4.1% | 4.4% | 4.5% | 4.6% | 4.7% | 4.8% | 4.9% | 5.0% | 5.0% | 5.0% | 5.0% | 5.1% |
| UK | 10467 | 3.4% | 3.9% | 4.2% | 4.4% | 4.5% | 4.6% | 4.7% | 4.8% | 4.8% | 4.9% | 4.9% | 4.9% | 4.9% | 4.9% |
| France | 10454 | 3.3% | 3.8% | 4.1% | 4.3% | 4.4% | 4.6% | 4.6% | 4.7% | 4.7% | 4.8% | 4.8% | 4.8% | 4.8% | 4.8% |
plot_conversion(conversion_region, conversion_region_history, horizon_days, window=7)
Вывод. Конверсия пользователей по регионам¶
Конверсия пользователей в США 9.6% существенно выше конверсии в других странах 4.8 - 5.1%.
Наибольшие скачки конверсии 14-го дня по всем странам наблюдались во второй половине мая. Показатель достигал 9 - 11%.
В дальнейшем конверсия по Великобритании, Германии, Франции понизилась до уровня 5%, в США сохранилась на уровне 8%.
Любопытно, что динамика конверсии США становится с течением времени не такой волатильной.
Конверсия пользователей по каналам продвижения¶
Проведем вычисления.
conversion_channel_raw, conversion_channel, conversion_channel_history = get_conversion(
profiles_ad, orders, observation_date, horizon_days, dimensions=['channel']
)
disp(conversion_channel, horizon_days)
| cohort_size | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| channel | |||||||||||||||
| FaceBoom | 27349 | 10.2% | 10.9% | 11.1% | 11.3% | 11.5% | 11.5% | 11.6% | 11.7% | 11.7% | 11.8% | 11.8% | 11.9% | 11.9% | 11.9% |
| AdNonSense | 3687 | 8.7% | 9.4% | 9.7% | 10.1% | 10.3% | 10.4% | 10.6% | 10.6% | 10.6% | 10.7% | 10.7% | 10.7% | 10.8% | 10.8% |
| lambdaMediaAds | 2049 | 7.3% | 8.2% | 8.5% | 8.9% | 9.3% | 9.6% | 10.0% | 10.1% | 10.3% | 10.3% | 10.3% | 10.4% | 10.4% | 10.4% |
| TipTop | 18367 | 5.7% | 6.9% | 7.6% | 8.1% | 8.4% | 8.7% | 8.8% | 8.9% | 9.1% | 9.2% | 9.2% | 9.3% | 9.3% | 9.4% |
| RocketSuperAds | 4225 | 3.4% | 4.7% | 5.4% | 5.9% | 6.4% | 6.6% | 6.7% | 6.9% | 7.0% | 7.1% | 7.3% | 7.3% | 7.3% | 7.4% |
| WahooNetBanner | 8029 | 3.1% | 3.8% | 4.3% | 4.5% | 4.7% | 4.8% | 4.9% | 4.9% | 5.0% | 5.1% | 5.1% | 5.1% | 5.1% | 5.2% |
| YRabbit | 4077 | 2.6% | 2.9% | 3.1% | 3.2% | 3.3% | 3.4% | 3.5% | 3.5% | 3.6% | 3.6% | 3.6% | 3.7% | 3.7% | 3.7% |
| MediaTornado | 4148 | 2.3% | 2.7% | 2.8% | 3.0% | 3.1% | 3.1% | 3.1% | 3.3% | 3.3% | 3.3% | 3.4% | 3.4% | 3.4% | 3.4% |
| LeapBob | 8047 | 1.7% | 2.1% | 2.4% | 2.5% | 2.6% | 2.7% | 2.7% | 2.8% | 2.8% | 2.8% | 2.9% | 2.9% | 2.9% | 2.9% |
| OppleCreativeMedia | 8078 | 1.7% | 2.0% | 2.2% | 2.3% | 2.4% | 2.5% | 2.5% | 2.6% | 2.6% | 2.6% | 2.6% | 2.6% | 2.6% | 2.6% |
Простроим графики.
plot_conversion(conversion_channel, conversion_channel_history, horizon_days, window=7)
Вывод. Конверсия пользователей по каналам продвижения¶
Конверсия пользователей в платящих пользователей по каналам FaceBoom 11.9%, AdNonSense 10.8%, lambdaMediaAds 10.4%, TipTop 9.4%, RocketSuperAds 7.4% существенно выше, чем у других каналов.
Прочие каналы привлечения дают конверсию до 5.2%.
Конверсии пользователей на 14-й день от месяца к месяцу по каналам FaceBoom, AdNonSense, lambdaMediaAds, TipTop, RocketSuperAds меняется значительно больше, чем по другим каналам. Вероятно, это связано с активной конкуренцией на каналах.
Удержание пользователей и динамика изменения удержания¶
Посчитаем удержание пользователей и выведем график.
retention_raw, retention, retention_history = get_retention(profiles_ad, visits, observation_date, horizon_days)
disp(retention, horizon_days)
| cohort_size | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| payer | |||||||||||||||
| True | 7259 | 100.0% | 38.1% | 29.3% | 23.6% | 21.0% | 19.1% | 16.7% | 15.7% | 14.5% | 13.6% | 13.5% | 12.2% | 11.9% | 10.6% |
| False | 80797 | 100.0% | 17.7% | 8.6% | 5.2% | 3.3% | 2.5% | 1.9% | 1.4% | 1.2% | 1.0% | 0.8% | 0.6% | 0.6% | 0.5% |
plot_retention(retention, retention_history, horizon_days, window=7)
Вывод. Удержание пользователей и динамика изменения удержания¶
Наибольшее падение удержания пользователей наблюдается в первые три дня жизненного цикла клиента. Далее удержание падает более плавно.
Удержание платящих пользователей в первые пять дней в 3 - 4 раз выше, чем неплатящих.
На 14-й день удержание неплатящих пользователей стремится к нулю, удержание платящих сохраняется на уровне около 10%.
Удержание платящих пользователей на 14-й день колеблется от недели к неделе. Пики приходились на конец мая 17.5%, средину июня 12.5%, вторую половину июля 13%, средину августа 15%, средину сентября 13%.
Удержание пользователей по устройствам и динамика изменения удержания¶
Выполним расчеты.
retention_raw, retention, retention_history = get_retention(profiles_ad, visits,
observation_date, horizon_days,
dimensions=['device'])
disp(retention.query('payer == True'), horizon_days)
disp(retention.query('payer == False'), horizon_days)
| cohort_size | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| payer | device | |||||||||||||||
| True | PC | 1201 | 100.0% | 42.0% | 33.2% | 26.4% | 26.1% | 21.0% | 22.6% | 19.2% | 17.4% | 15.0% | 16.4% | 14.7% | 14.4% | 12.5% |
| Mac | 1590 | 100.0% | 36.5% | 28.6% | 20.3% | 19.7% | 17.2% | 14.8% | 14.3% | 13.7% | 13.2% | 12.1% | 11.5% | 10.9% | 11.1% | |
| Android | 1681 | 100.0% | 38.6% | 29.7% | 25.0% | 21.5% | 20.7% | 15.8% | 16.7% | 14.7% | 13.9% | 13.3% | 11.2% | 12.9% | 10.8% | |
| iPhone | 2787 | 100.0% | 37.0% | 27.7% | 23.4% | 19.4% | 18.5% | 15.8% | 14.4% | 13.6% | 13.2% | 13.2% | 12.1% | 10.7% | 9.3% |
| cohort_size | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| payer | device | |||||||||||||||
| False | PC | 16805 | 100.0% | 17.5% | 8.7% | 5.2% | 3.5% | 2.4% | 1.8% | 1.5% | 1.1% | 0.9% | 0.7% | 0.5% | 0.6% | 0.6% |
| Android | 18865 | 100.0% | 17.5% | 8.6% | 5.2% | 3.1% | 2.4% | 1.7% | 1.4% | 1.1% | 1.0% | 0.8% | 0.7% | 0.6% | 0.5% | |
| iPhone | 29061 | 100.0% | 17.8% | 8.7% | 5.3% | 3.4% | 2.6% | 1.9% | 1.4% | 1.2% | 1.0% | 0.7% | 0.7% | 0.6% | 0.4% | |
| Mac | 16066 | 100.0% | 17.9% | 8.6% | 4.9% | 3.3% | 2.4% | 1.9% | 1.5% | 1.3% | 0.9% | 0.8% | 0.6% | 0.6% | 0.4% |
Построим графики.
plot_retention(retention, retention_history, horizon_days, window=7)
Вывод. Удержание пользователей по устройствам и динамика изменения удержания¶
Сред платящих пользователей более хорошее удержание наблюдаем для устройств PC 12.5%. На втором месте Android. Хуже всего удержание в динамике у устройств Mac и iPhone.
В динамике от месяца к месяцу больше всего колеблется удержание для PC и Mac - до 20%. Для мобильных устройств колебания доходят до 15%. Изменения удержания, вероятно, связаны с доработками приложения.
Удержание неплатящих пользователей по всем устройствам соизмеримы и стремятся на 14-й день к 0.4 - 0.6%.
Удержание пользователей по регионам и динамика изменения удержания¶
Выполним расчеты.
retention_raw, retention, retention_history = get_retention(profiles_ad, visits,
observation_date, horizon_days,
dimensions=['region'])
disp(retention.query('payer == True'), horizon_days)
disp(retention.query('payer == False'), horizon_days)
| cohort_size | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| payer | region | |||||||||||||||
| True | UK | 530 | 100.0% | 48.5% | 42.1% | 33.0% | 29.8% | 27.9% | 27.0% | 24.9% | 19.8% | 15.7% | 17.0% | 19.2% | 18.5% | 17.9% |
| France | 521 | 100.0% | 47.4% | 38.6% | 32.4% | 30.9% | 29.6% | 24.4% | 23.8% | 22.3% | 20.5% | 20.9% | 16.9% | 18.6% | 17.5% | |
| Germany | 464 | 100.0% | 48.1% | 40.1% | 31.7% | 32.8% | 28.0% | 24.6% | 22.2% | 23.9% | 21.1% | 22.8% | 17.5% | 15.9% | 13.6% | |
| United States | 5744 | 100.0% | 35.5% | 26.4% | 21.3% | 18.4% | 16.7% | 14.4% | 13.6% | 12.6% | 12.2% | 11.8% | 10.7% | 10.3% | 9.0% |
| cohort_size | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| payer | region | |||||||||||||||
| False | Germany | 8505 | 100.0% | 17.6% | 8.5% | 5.6% | 3.5% | 2.5% | 1.9% | 1.4% | 1.0% | 1.0% | 0.7% | 0.6% | 0.7% | 0.7% |
| France | 9933 | 100.0% | 17.4% | 9.0% | 5.2% | 3.4% | 2.3% | 1.8% | 1.5% | 1.2% | 1.1% | 0.8% | 0.6% | 0.5% | 0.6% | |
| UK | 9937 | 100.0% | 17.9% | 8.9% | 5.1% | 3.2% | 2.5% | 1.8% | 1.6% | 1.2% | 0.9% | 0.7% | 0.7% | 0.6% | 0.5% | |
| United States | 52422 | 100.0% | 17.7% | 8.6% | 5.1% | 3.3% | 2.5% | 1.9% | 1.4% | 1.2% | 0.9% | 0.8% | 0.6% | 0.6% | 0.4% |
Построим графики.
plot_retention(retention, retention_history, horizon_days, window=7)
Вывод. Удержание пользователей по регионам и динамика изменения удержания¶
Самое низкое удержание платящих пользователей на 14-й день в США - 9%. Удержание Германии 13.6%, Франции 17.5%, Великобритании 17.9%. Удержание в Великобритании проседает на 9-й день, Германии на 11-й день.
В СЩА каждый день есть пользователи, дожившие до 14-го дня "жизни" в приложении. По другим странам такое выполняется не всегда.
Удержание неплатящих пользователей соизмеримо по всем странам и стремится к 0.4 - 0.7% на 14-й день.
Удержание пользователей по каналам продвижения и динамика изменения удержания¶
Посчитаем удержание пользователей и выведем график.
retention_raw, retention, retention_history = get_retention(profiles_ad, visits,
observation_date, horizon_days,
dimensions=['channel'])
disp(retention.query('payer == True'), horizon_days)
disp(retention.query('payer == False'), horizon_days)
| cohort_size | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| payer | channel | |||||||||||||||
| True | WahooNetBanner | 423 | 100.0% | 55.8% | 51.1% | 39.2% | 44.9% | 38.5% | 33.8% | 34.8% | 31.2% | 26.2% | 30.7% | 27.0% | 25.3% | 23.2% |
| RocketSuperAds | 333 | 100.0% | 57.4% | 46.8% | 39.9% | 39.6% | 32.7% | 32.1% | 32.1% | 26.4% | 28.2% | 31.8% | 22.5% | 27.6% | 22.2% | |
| OppleCreativeMedia | 219 | 100.0% | 53.4% | 46.1% | 40.2% | 39.7% | 31.5% | 33.3% | 27.4% | 23.7% | 22.8% | 24.2% | 22.4% | 23.3% | 21.5% | |
| LeapBob | 240 | 100.0% | 55.0% | 49.2% | 40.0% | 33.3% | 38.8% | 28.7% | 25.0% | 27.1% | 25.0% | 27.5% | 24.2% | 21.7% | 21.2% | |
| TipTop | 1774 | 100.0% | 53.4% | 46.3% | 40.9% | 35.2% | 34.7% | 29.0% | 28.7% | 27.8% | 26.3% | 24.9% | 24.0% | 22.7% | 20.7% | |
| lambdaMediaAds | 220 | 100.0% | 56.4% | 49.1% | 39.5% | 37.3% | 37.3% | 34.1% | 32.7% | 27.3% | 21.4% | 22.3% | 18.2% | 20.5% | 19.1% | |
| MediaTornado | 148 | 100.0% | 50.0% | 45.3% | 35.1% | 34.5% | 35.1% | 25.0% | 23.0% | 21.6% | 17.6% | 23.0% | 23.0% | 16.2% | 12.2% | |
| YRabbit | 155 | 100.0% | 56.1% | 38.1% | 40.0% | 36.8% | 27.7% | 26.5% | 22.6% | 26.5% | 25.8% | 21.9% | 21.3% | 21.3% | 10.3% | |
| AdNonSense | 413 | 100.0% | 28.6% | 16.2% | 13.1% | 7.7% | 6.1% | 5.8% | 4.8% | 5.6% | 4.8% | 1.7% | 2.4% | 3.4% | 2.7% | |
| FaceBoom | 3334 | 100.0% | 22.2% | 12.3% | 7.5% | 5.8% | 4.1% | 3.9% | 2.8% | 2.0% | 2.3% | 1.8% | 1.4% | 1.2% | 1.3% |
| cohort_size | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| payer | channel | |||||||||||||||
| False | AdNonSense | 3274 | 100.0% | 17.6% | 8.7% | 5.1% | 3.7% | 2.2% | 1.9% | 1.7% | 1.1% | 1.1% | 0.6% | 0.5% | 0.7% | 0.7% |
| OppleCreativeMedia | 7859 | 100.0% | 17.2% | 8.9% | 5.3% | 3.2% | 2.6% | 1.9% | 1.5% | 1.2% | 0.8% | 0.8% | 0.5% | 0.7% | 0.6% | |
| lambdaMediaAds | 1829 | 100.0% | 18.4% | 8.7% | 6.3% | 3.3% | 3.1% | 1.6% | 1.4% | 1.1% | 0.9% | 0.8% | 0.9% | 0.3% | 0.5% | |
| WahooNetBanner | 7606 | 100.0% | 18.4% | 8.5% | 5.3% | 3.4% | 2.2% | 1.7% | 1.5% | 1.2% | 1.0% | 0.7% | 0.6% | 0.6% | 0.5% | |
| YRabbit | 3922 | 100.0% | 18.0% | 8.3% | 4.9% | 3.5% | 2.3% | 1.6% | 1.4% | 0.9% | 0.8% | 0.6% | 0.6% | 0.5% | 0.5% | |
| MediaTornado | 4000 | 100.0% | 18.3% | 8.2% | 5.6% | 2.9% | 2.5% | 1.8% | 1.2% | 1.6% | 0.9% | 0.7% | 0.9% | 0.4% | 0.5% | |
| LeapBob | 7807 | 100.0% | 17.1% | 9.1% | 5.1% | 3.3% | 2.3% | 1.9% | 1.5% | 1.1% | 1.1% | 0.7% | 0.7% | 0.5% | 0.5% | |
| FaceBoom | 24015 | 100.0% | 17.3% | 8.7% | 5.1% | 3.3% | 2.5% | 2.0% | 1.4% | 1.2% | 1.0% | 0.8% | 0.5% | 0.7% | 0.4% | |
| TipTop | 16593 | 100.0% | 18.1% | 8.5% | 5.1% | 3.5% | 2.6% | 1.9% | 1.4% | 1.2% | 1.0% | 0.7% | 0.7% | 0.6% | 0.4% | |
| RocketSuperAds | 3892 | 100.0% | 17.6% | 8.6% | 5.1% | 3.5% | 2.1% | 1.5% | 1.6% | 0.8% | 0.8% | 0.7% | 0.8% | 0.4% | 0.3% |
Построим графики.
plot_retention(retention, retention_history, horizon_days, window=7)
Вывод. Удержание пользователей по каналам продвижения и динамика изменения удержания¶
Удержание платящих пользователей на каналах AdNonSense и FaceBoom самое низкое по сравнению с другими каналами. На четвертый день жизни в нашем приложении удержание AdNonSense 13.1%, FaceBoom 7.5% против 35.1% - 40.2% в других каналах, на 14-й AdNonSense 2.7%, FaceBoom 1.3%.
Вероятно, пользователи, приходящие с AdNonSense и FaceBoom больше ориентированы на другие решения.
Удержание TipTop на 14-й день 20.7%.
Удержание платящих пользователей на 14-й день меняется от месяца к месяцу. Не по каждому каналу есть ежедневно платящие пользователи, "дожившие" до 14-го дня. Завидной стабильностью в этом смысле отличаются только каналы TipTop и FaceBoom.
Удержание неплатящих пользователей одинаковое по всем каналам привлечения и на 14-й день и стремится к 0.5%. Поэтому ключевая задача - побудить пользователя заплатить в приложении в первые три дня его "жизни".
Удержание пользователей по странам и каналам продвижения, динамика изменения удержания¶
Проведем вычисления.
retention_raw, retention, retention_history = get_retention(profiles_ad, visits,
observation_date, horizon_days,
dimensions=['region', 'channel'])
# Выберем только платящих пользователей
result = retention.query('payer == True')
# Расчитаем удержанный размер когорты на конец horizon_days
result.insert(loc=1, column='cohort_size_end', value=(result['cohort_size'] * result[horizon_days-1]))
# Выведем на экран
(result
.style
.format(formatter='{:.1%}', subset=list(range(horizon_days)))
.format(formatter='{:.0f}', subset=['cohort_size', 'cohort_size_end'])
)
| cohort_size | cohort_size_end | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | |||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| payer | region | channel | ||||||||||||||||
| True | France | AdNonSense | 141 | 4 | 100.0% | 27.0% | 14.9% | 11.3% | 5.7% | 7.8% | 2.1% | 3.5% | 5.0% | 6.4% | 1.4% | 3.5% | 3.5% | 2.8% |
| LeapBob | 85 | 16 | 100.0% | 54.1% | 52.9% | 35.3% | 34.1% | 35.3% | 21.2% | 22.4% | 29.4% | 24.7% | 24.7% | 24.7% | 20.0% | 18.8% | ||
| OppleCreativeMedia | 74 | 18 | 100.0% | 55.4% | 43.2% | 40.5% | 35.1% | 28.4% | 36.5% | 28.4% | 25.7% | 24.3% | 20.3% | 21.6% | 25.7% | 24.3% | ||
| WahooNetBanner | 140 | 37 | 100.0% | 55.7% | 50.7% | 42.1% | 47.9% | 44.3% | 37.1% | 34.3% | 31.4% | 28.6% | 35.7% | 25.7% | 25.7% | 26.4% | ||
| lambdaMediaAds | 81 | 16 | 100.0% | 54.3% | 39.5% | 42.0% | 38.3% | 37.0% | 33.3% | 38.3% | 25.9% | 23.5% | 25.9% | 12.3% | 24.7% | 19.8% | ||
| Germany | AdNonSense | 132 | 3 | 100.0% | 27.3% | 15.9% | 14.4% | 10.6% | 3.8% | 6.8% | 6.1% | 6.8% | 5.3% | 3.0% | 2.3% | 4.5% | 2.3% | |
| LeapBob | 61 | 9 | 100.0% | 55.7% | 47.5% | 49.2% | 29.5% | 41.0% | 34.4% | 19.7% | 26.2% | 32.8% | 29.5% | 24.6% | 18.0% | 14.8% | ||
| OppleCreativeMedia | 61 | 11 | 100.0% | 49.2% | 54.1% | 36.1% | 45.9% | 36.1% | 26.2% | 21.3% | 23.0% | 21.3% | 31.1% | 21.3% | 13.1% | 18.0% | ||
| WahooNetBanner | 144 | 31 | 100.0% | 58.3% | 50.0% | 34.0% | 47.2% | 36.1% | 31.9% | 36.1% | 33.3% | 31.2% | 31.9% | 23.6% | 25.7% | 21.5% | ||
| lambdaMediaAds | 66 | 9 | 100.0% | 59.1% | 47.0% | 40.9% | 36.4% | 39.4% | 33.3% | 27.3% | 36.4% | 19.7% | 28.8% | 24.2% | 18.2% | 13.6% | ||
| UK | AdNonSense | 140 | 4 | 100.0% | 31.4% | 17.9% | 13.6% | 7.1% | 6.4% | 8.6% | 5.0% | 5.0% | 2.9% | 0.7% | 1.4% | 2.1% | 2.9% | |
| LeapBob | 94 | 26 | 100.0% | 55.3% | 46.8% | 38.3% | 35.1% | 40.4% | 31.9% | 30.9% | 25.5% | 20.2% | 28.7% | 23.4% | 25.5% | 27.7% | ||
| OppleCreativeMedia | 84 | 18 | 100.0% | 54.8% | 42.9% | 42.9% | 39.3% | 31.0% | 35.7% | 31.0% | 22.6% | 22.6% | 22.6% | 23.8% | 28.6% | 21.4% | ||
| WahooNetBanner | 139 | 30 | 100.0% | 53.2% | 52.5% | 41.7% | 39.6% | 35.3% | 32.4% | 33.8% | 28.8% | 18.7% | 24.5% | 31.7% | 24.5% | 21.6% | ||
| lambdaMediaAds | 73 | 17 | 100.0% | 56.2% | 61.6% | 35.6% | 37.0% | 35.6% | 35.6% | 31.5% | 20.5% | 20.5% | 12.3% | 19.2% | 17.8% | 23.3% | ||
| United States | FaceBoom | 3334 | 42 | 100.0% | 22.2% | 12.3% | 7.5% | 5.8% | 4.1% | 3.9% | 2.8% | 2.0% | 2.3% | 1.8% | 1.4% | 1.2% | 1.3% | |
| MediaTornado | 148 | 18 | 100.0% | 50.0% | 45.3% | 35.1% | 34.5% | 35.1% | 25.0% | 23.0% | 21.6% | 17.6% | 23.0% | 23.0% | 16.2% | 12.2% | ||
| RocketSuperAds | 333 | 74 | 100.0% | 57.4% | 46.8% | 39.9% | 39.6% | 32.7% | 32.1% | 32.1% | 26.4% | 28.2% | 31.8% | 22.5% | 27.6% | 22.2% | ||
| TipTop | 1774 | 368 | 100.0% | 53.4% | 46.3% | 40.9% | 35.2% | 34.7% | 29.0% | 28.7% | 27.8% | 26.3% | 24.9% | 24.0% | 22.7% | 20.7% | ||
| YRabbit | 155 | 16 | 100.0% | 56.1% | 38.1% | 40.0% | 36.8% | 27.7% | 26.5% | 22.6% | 26.5% | 25.8% | 21.9% | 21.3% | 21.3% | 10.3% |
Вывод. Удержание пользователей по странам и каналам продвижения, динамика изменения удержания¶
По региону США трафик приводят каналы FaceBoom, MediaTornado, RocketSuperAds, TipTop, YRabbit.
На 14-й день наихудшее удержание у канала FaceBoom 1.3%, из привлеченных 3334 человек удержано 42.
Для сравнения у канала RocketSuperAds с удержанием 22.2% из привлеченных 333 человек удержано 74.
Аналогично низким удержанием по другим регионам отличается канал AdNonSense. Франция удержание 2.8%, Германия - 2.3%, Великобритания - 2.9%.
Окупаемость рекламы с разбивкой по устройствам¶
Проведем расчеты.
ltv_dev_raw, ltv_dev, ltv_dev_history, roi_dev, roi_dev_history = get_ltv(profiles_ad, orders,
observation_date, horizon_days, dimensions=['device'])
print('ltv_dev')
display(ltv_dev)
print('roi_dev')
disp(roi_dev, horizon_days)
ltv_dev
| cohort_size | 0.0 | 1.0 | 2.0 | 3.0 | 4.0 | 5.0 | 6.0 | 7.0 | 8.0 | 9.0 | 10.0 | 11.0 | 12.0 | 13.0 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| device | |||||||||||||||
| Android | 20546 | 0.338516 | 0.430432 | 0.499367 | 0.554014 | 0.607308 | 0.660413 | 0.699084 | 0.737852 | 0.773269 | 0.806353 | 0.833554 | 0.855898 | 0.887771 | 0.912059 |
| Mac | 17656 | 0.383074 | 0.480716 | 0.550981 | 0.605935 | 0.652293 | 0.686490 | 0.722725 | 0.753928 | 0.792208 | 0.826521 | 0.858686 | 0.885536 | 0.905888 | 0.931946 |
| PC | 18006 | 0.278171 | 0.361990 | 0.430789 | 0.486393 | 0.540446 | 0.582737 | 0.632413 | 0.663341 | 0.702095 | 0.730698 | 0.763012 | 0.789839 | 0.813396 | 0.837620 |
| iPhone | 31848 | 0.367544 | 0.456270 | 0.523744 | 0.582822 | 0.631842 | 0.673269 | 0.710691 | 0.749461 | 0.779517 | 0.811703 | 0.839500 | 0.866544 | 0.892936 | 0.915687 |
roi_dev
| cohort_size | cac | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| device | ||||||||||||||||
| PC | 18006 | $ 0.79 | 35.1% | 45.7% | 54.4% | 61.4% | 68.2% | 73.6% | 79.8% | 83.7% | 88.6% | 92.3% | 96.3% | 99.7% | 102.7% | 105.8% |
| Android | 20546 | $ 1.02 | 33.2% | 42.3% | 49.0% | 54.4% | 59.6% | 64.9% | 68.7% | 72.5% | 75.9% | 79.2% | 81.9% | 84.1% | 87.2% | 89.6% |
| Mac | 17656 | $ 1.27 | 30.1% | 37.7% | 43.2% | 47.5% | 51.2% | 53.9% | 56.7% | 59.1% | 62.2% | 64.8% | 67.4% | 69.5% | 71.1% | 73.1% |
| iPhone | 31848 | $ 1.28 | 28.8% | 35.8% | 41.1% | 45.7% | 49.5% | 52.8% | 55.7% | 58.8% | 61.1% | 63.7% | 65.8% | 68.0% | 70.0% | 71.8% |
Построим графики.
plot_ltv_roi(ltv_dev, ltv_dev_history, roi_dev, roi_dev_history, horizon_days, window=7)
Вывод. Окупаемость рекламы с разбивкой по устройствам¶
Выручка по клиенту на 14-й день соизмерима для всех мобильных платформ $ 0.9 и чуть ниже для PC 0.8.
По месяцам выручка с клиента на 14-й день колеблется от $ 0.8 до 1.1, но есть провалы для платформы PC в средине июля до 0.4, конце августа - начале сентября до 0.7, в начале октября до 0.5.
Стоимость привлечения пользователей для всех четырех платформ в мае была 0.7 - 0.8 долларов. С июня она стала неуклонно расти до значений в октябре:
- PC $ 0.9
- Android $ 1.1
- Mac, iPhone $ 1.5
В рамках предусмотренных бизнес-планом четырнадцати дней реклама не окупается для устройств Mac, iPhone, Android. Такая тенденция наблюдается с июля. Рентабельны были только вложения в платформу PC, но с сентября они тоже ушли в минус.
Окупаемость рекламы с разбивкой по странам¶
Проведем расчеты.
ltv_region_raw, ltv_region, ltv_region_history, roi_region, roi_region_history = get_ltv(
profiles_ad, orders, observation_date, horizon_days, dimensions=['region']
)
print('ltv_region')
display(ltv_region)
print('roi_region')
disp(roi_region, horizon_days)
ltv_region
| cohort_size | 0.0 | 1.0 | 2.0 | 3.0 | 4.0 | 5.0 | 6.0 | 7.0 | 8.0 | 9.0 | 10.0 | 11.0 | 12.0 | 13.0 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| region | |||||||||||||||
| France | 10454 | 0.194098 | 0.260847 | 0.313068 | 0.361858 | 0.408828 | 0.452366 | 0.483395 | 0.509266 | 0.543264 | 0.572860 | 0.602169 | 0.624127 | 0.651338 | 0.671866 |
| Germany | 8969 | 0.186505 | 0.258393 | 0.327393 | 0.375245 | 0.422870 | 0.460704 | 0.496980 | 0.534368 | 0.571094 | 0.605589 | 0.641419 | 0.666011 | 0.691050 | 0.712861 |
| UK | 10467 | 0.219171 | 0.297764 | 0.368143 | 0.417446 | 0.466190 | 0.504902 | 0.556895 | 0.586644 | 0.624515 | 0.646445 | 0.669902 | 0.693072 | 0.720731 | 0.744569 |
| United States | 58166 | 0.420127 | 0.519536 | 0.590767 | 0.651292 | 0.703204 | 0.747493 | 0.787493 | 0.825692 | 0.859687 | 0.893682 | 0.923263 | 0.950511 | 0.975960 | 1.001063 |
roi_region
| cohort_size | cac | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| region | ||||||||||||||||
| UK | 10467 | $ 0.46 | 48.2% | 65.4% | 80.9% | 91.7% | 102.4% | 110.9% | 122.4% | 128.9% | 137.2% | 142.0% | 147.2% | 152.3% | 158.4% | 163.6% |
| Germany | 8969 | $ 0.46 | 40.1% | 55.6% | 70.4% | 80.7% | 91.0% | 99.1% | 106.9% | 114.9% | 122.8% | 130.3% | 138.0% | 143.3% | 148.6% | 153.3% |
| France | 10454 | $ 0.46 | 42.2% | 56.7% | 68.0% | 78.6% | 88.8% | 98.3% | 105.0% | 110.6% | 118.0% | 124.4% | 130.8% | 135.6% | 141.5% | 145.9% |
| United States | 58166 | $ 1.45 | 28.9% | 35.7% | 40.6% | 44.8% | 48.4% | 51.4% | 54.2% | 56.8% | 59.1% | 61.5% | 63.5% | 65.4% | 67.1% | 68.9% |
Построим графики.
plot_ltv_roi(ltv_region, ltv_region_history, roi_region, roi_region_history, horizon_days, window=7)
Вывод. Окупаемость рекламы с разбивкой по странам¶
Больше всего платят пользователи из США - 1 доллар на 14-й день "жизни" в приложении, меньше всего из Франции 0.67 доллара. Пользователи из Германии и Великобритании тратят соизмеримые суммы - около 0.7 доллара.
LTV 14-го дня существенно колеблется от месяца к месяцу. Наибольшая волатильность у Великобритании - от 0.8 до 1.3 доллара. Колебания LTV США наблюдаются от 0.8 до 1.2 доллара, Германии - от 0.4 до 0.9, Франции - от 0.4 до 1 доллара.
Стоимость привлечения пользователей для Франции, Германии, Великобритании соизмерима и составила 0.65 доллара в мае и затем понизилась до 0.45 долларов.
Стоимость привлечения для США с 0.8 в начале мая достигла 1.2 в начале июня и далее продолжила расти до 1.7 - 1.8 доллара в сентябре, октябре.
В предусмотренный бизнес-планом четырнадцати дневный срок с момента привлечения пользователя инвестиции в рекламу окупаются для Великобритании, Германии, Франции и не окупаются для пользователей из США.
Провал ROI 14-го дня для США начался в середине мая.
Окупаемость рекламы с разбивкой по рекламным каналам¶
Проведем расчеты.
ltv_channel_raw, ltv_channel, ltv_channel_history, roi_channel, roi_channel_history = get_ltv(
profiles_ad, orders, observation_date, horizon_days, dimensions=['channel']
)
print('ltv_channel')
display(ltv_channel)
print('roi_channel')
disp(roi_channel, horizon_days)
ltv_channel
| cohort_size | 0.0 | 1.0 | 2.0 | 3.0 | 4.0 | 5.0 | 6.0 | 7.0 | 8.0 | 9.0 | 10.0 | 11.0 | 12.0 | 13.0 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| channel | |||||||||||||||
| AdNonSense | 3687 | 0.507084 | 0.591546 | 0.647038 | 0.686290 | 0.709569 | 0.735563 | 0.757217 | 0.770751 | 0.792414 | 0.805948 | 0.810279 | 0.818400 | 0.830580 | 0.841416 |
| FaceBoom | 27349 | 0.585464 | 0.660355 | 0.699989 | 0.727945 | 0.746376 | 0.759514 | 0.773965 | 0.784076 | 0.791595 | 0.800026 | 0.805537 | 0.811558 | 0.816670 | 0.820684 |
| LeapBob | 8047 | 0.106553 | 0.154925 | 0.204533 | 0.234548 | 0.258981 | 0.287507 | 0.313676 | 0.333025 | 0.368394 | 0.393199 | 0.416144 | 0.436609 | 0.455212 | 0.470715 |
| MediaTornado | 4148 | 0.138115 | 0.192493 | 0.248074 | 0.295012 | 0.333770 | 0.368660 | 0.390313 | 0.420391 | 0.442049 | 0.463703 | 0.487765 | 0.510622 | 0.528667 | 0.536128 |
| OppleCreativeMedia | 8078 | 0.097356 | 0.131953 | 0.172364 | 0.207579 | 0.242306 | 0.267633 | 0.291726 | 0.311247 | 0.330407 | 0.348940 | 0.366360 | 0.378839 | 0.397382 | 0.411590 |
| RocketSuperAds | 4225 | 0.208847 | 0.319420 | 0.397370 | 0.456424 | 0.516424 | 0.569808 | 0.638334 | 0.685579 | 0.736622 | 0.787893 | 0.829467 | 0.862540 | 0.894665 | 0.930097 |
| TipTop | 18367 | 0.343283 | 0.497948 | 0.624789 | 0.739948 | 0.842660 | 0.936131 | 1.014657 | 1.097478 | 1.172419 | 1.246489 | 1.312076 | 1.371739 | 1.428313 | 1.489010 |
| WahooNetBanner | 8029 | 0.191321 | 0.291537 | 0.378315 | 0.443576 | 0.516545 | 0.569499 | 0.631690 | 0.677681 | 0.723926 | 0.764326 | 0.811684 | 0.848104 | 0.887885 | 0.917221 |
| YRabbit | 4077 | 0.163078 | 0.212284 | 0.253897 | 0.302124 | 0.354778 | 0.386600 | 0.413527 | 0.438008 | 0.460039 | 0.479622 | 0.501653 | 0.523689 | 0.545965 | 0.559429 |
| lambdaMediaAds | 2049 | 0.461840 | 0.647506 | 0.781449 | 0.908633 | 1.045061 | 1.164397 | 1.266681 | 1.356789 | 1.449824 | 1.510708 | 1.584744 | 1.635886 | 1.701654 | 1.772284 |
roi_channel
| cohort_size | cac | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| channel | ||||||||||||||||
| YRabbit | 4077 | $ 0.22 | 75.1% | 97.8% | 117.0% | 139.2% | 163.5% | 178.1% | 190.5% | 201.8% | 212.0% | 221.0% | 231.1% | 241.3% | 251.6% | 257.8% |
| MediaTornado | 4148 | $ 0.22 | 63.0% | 87.8% | 113.2% | 134.6% | 152.2% | 168.2% | 178.0% | 191.8% | 201.6% | 211.5% | 222.5% | 232.9% | 241.1% | 244.6% |
| lambdaMediaAds | 2049 | $ 0.73 | 63.5% | 89.1% | 107.5% | 125.0% | 143.8% | 160.2% | 174.2% | 186.6% | 199.4% | 207.8% | 218.0% | 225.0% | 234.1% | 243.8% |
| LeapBob | 8047 | $ 0.21 | 50.7% | 73.8% | 97.4% | 111.7% | 123.3% | 136.9% | 149.4% | 158.6% | 175.4% | 187.2% | 198.1% | 207.9% | 216.7% | 224.1% |
| RocketSuperAds | 4225 | $ 0.42 | 50.0% | 76.4% | 95.0% | 109.2% | 123.5% | 136.3% | 152.7% | 164.0% | 176.2% | 188.5% | 198.4% | 206.3% | 214.0% | 222.5% |
| OppleCreativeMedia | 8078 | $ 0.25 | 39.0% | 52.8% | 69.0% | 83.1% | 97.0% | 107.1% | 116.8% | 124.6% | 132.2% | 139.7% | 146.6% | 151.6% | 159.1% | 164.7% |
| WahooNetBanner | 8029 | $ 0.60 | 31.8% | 48.4% | 62.9% | 73.7% | 85.8% | 94.6% | 105.0% | 112.6% | 120.3% | 127.0% | 134.9% | 140.9% | 147.5% | 152.4% |
| AdNonSense | 3687 | $ 1.01 | 50.3% | 58.7% | 64.2% | 68.1% | 70.4% | 73.0% | 75.1% | 76.5% | 78.6% | 80.0% | 80.4% | 81.2% | 82.4% | 83.5% |
| FaceBoom | 27349 | $ 1.11 | 52.6% | 59.3% | 62.9% | 65.4% | 67.0% | 68.2% | 69.5% | 70.4% | 71.1% | 71.9% | 72.3% | 72.9% | 73.3% | 73.7% |
| TipTop | 18367 | $ 2.75 | 12.5% | 18.1% | 22.7% | 26.9% | 30.6% | 34.0% | 36.9% | 39.9% | 42.6% | 45.3% | 47.7% | 49.9% | 51.9% | 54.1% |
Построим графики.
plot_ltv_roi(ltv_channel, ltv_channel_history, roi_channel, roi_channel_history, horizon_days, window=7)
Вывод Окупаемость рекламы с разбивкой по рекламным каналам¶
Наибольшая выручка 14-го дня наблюдается у рекламных каналов lambdaMediaAds 1.77, TipTop 1.49, RocketSuperAds 0.93 и WahooNetBanner 0.92, AdNonSense 0.84, FaceBoom 0.82. Прочие рекламные каналы дают выручку в районе 0.5 доллара с клиента.
Существенно колеблется выручка 14-го дня для канала продвижения lambdaMediaAds. Пики достигают в некоторые месяцы 3-5 долларов на пользователя. По остальным каналам колебания LTV не превышают от месяца к месяцу плюс-минус 0.5 доллара.
Динамика стоимости привлечения пользователей стабильная по всем каналам кроме TipTop. В канале TipTop стоимость привлечения росла ежемесячно и выросла с 1 доллара в мае до 3.5 долларов за пользователя в октябре.
Инвестиции в рекламу окупаются для каналов продвижения (в порядке убывания): YRabbit, MediaTornado, lambdaMediaAds, LeapBob, RocketSuperAds, OppleCreativeMedia, WahooNetBanner.
Инвестиции в рекламу не окупаются для каналов продвижения TipTop, FaceBoom, AdNonSense.
ROI на 14-й день существенно колеблется по большинству каналов продвижения. Наиболее высокие показатели ROI на 14-й день в пиках дают YRabbit, RocketSuperAds, MediaTornado. В среднем по рентабельным каналам продвижения ROI 200 - 300%, по особо рентабельным 500 - 700%.
Окупаемость рекламы с разбивкой по странам и рекламным каналам¶
Проведем расчеты.
ltv_region_channel_raw, ltv_region_channel, ltv_region_channel_history, roi_region_channel, roi_region_channel_history = get_ltv(
profiles_ad, orders, observation_date, horizon_days, dimensions=['region', 'channel']
)
print('ltv_region_channel')
display(ltv_region_channel)
ltv_region_channel
| cohort_size | 0.0 | 1.0 | 2.0 | 3.0 | 4.0 | 5.0 | 6.0 | 7.0 | 8.0 | 9.0 | 10.0 | 11.0 | 12.0 | 13.0 | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| region | channel | |||||||||||||||
| France | AdNonSense | 1305 | 0.468874 | 0.526996 | 0.584360 | 0.622605 | 0.641724 | 0.680751 | 0.688398 | 0.699870 | 0.703693 | 0.715165 | 0.719755 | 0.731226 | 0.738874 | 0.758015 |
| LeapBob | 2818 | 0.098112 | 0.147693 | 0.195504 | 0.224191 | 0.250752 | 0.277314 | 0.291480 | 0.303875 | 0.353488 | 0.380050 | 0.399528 | 0.419010 | 0.438488 | 0.449113 | |
| OppleCreativeMedia | 2811 | 0.092665 | 0.129957 | 0.158360 | 0.195649 | 0.224408 | 0.247485 | 0.270562 | 0.288670 | 0.302871 | 0.322401 | 0.338378 | 0.349029 | 0.361455 | 0.372106 | |
| WahooNetBanner | 2813 | 0.195844 | 0.299488 | 0.367608 | 0.429694 | 0.504554 | 0.568422 | 0.619876 | 0.655354 | 0.694390 | 0.738742 | 0.784863 | 0.820341 | 0.866477 | 0.894860 | |
| lambdaMediaAds | 707 | 0.465842 | 0.587256 | 0.679010 | 0.820212 | 0.961386 | 1.081372 | 1.173126 | 1.271938 | 1.358048 | 1.414512 | 1.514738 | 1.557086 | 1.634724 | 1.705304 | |
| Germany | AdNonSense | 1151 | 0.529861 | 0.625265 | 0.681625 | 0.720643 | 0.751859 | 0.769201 | 0.790877 | 0.803884 | 0.851599 | 0.873275 | 0.881946 | 0.886281 | 0.907958 | 0.916629 |
| LeapBob | 2361 | 0.087078 | 0.139928 | 0.186425 | 0.220246 | 0.241804 | 0.273511 | 0.297183 | 0.318323 | 0.328890 | 0.358479 | 0.379615 | 0.396523 | 0.407090 | 0.421885 | |
| OppleCreativeMedia | 2413 | 0.078997 | 0.105881 | 0.165889 | 0.196908 | 0.232064 | 0.256879 | 0.281699 | 0.302793 | 0.308997 | 0.325541 | 0.348703 | 0.363593 | 0.363593 | 0.378069 | |
| WahooNetBanner | 2425 | 0.177790 | 0.279031 | 0.367518 | 0.441608 | 0.520214 | 0.569600 | 0.616928 | 0.682775 | 0.744511 | 0.798016 | 0.856045 | 0.897200 | 0.948643 | 0.986099 | |
| lambdaMediaAds | 619 | 0.380517 | 0.541745 | 0.678788 | 0.759402 | 0.864200 | 0.968998 | 1.081858 | 1.178595 | 1.315654 | 1.388207 | 1.493005 | 1.557496 | 1.638158 | 1.678465 | |
| UK | AdNonSense | 1231 | 0.526296 | 0.628448 | 0.681145 | 0.721682 | 0.741950 | 0.762218 | 0.798700 | 0.814915 | 0.831129 | 0.839236 | 0.839236 | 0.847344 | 0.855451 | 0.859504 |
| LeapBob | 2868 | 0.130879 | 0.174376 | 0.228312 | 0.256499 | 0.281206 | 0.309045 | 0.349062 | 0.373769 | 0.415561 | 0.434700 | 0.462542 | 0.486900 | 0.511259 | 0.532137 | |
| OppleCreativeMedia | 2854 | 0.117498 | 0.155964 | 0.191633 | 0.228350 | 0.268595 | 0.296570 | 0.321048 | 0.340631 | 0.375631 | 0.394863 | 0.408851 | 0.421090 | 0.461335 | 0.478819 | |
| WahooNetBanner | 2791 | 0.198520 | 0.294389 | 0.398488 | 0.459276 | 0.525442 | 0.570498 | 0.656424 | 0.695758 | 0.735808 | 0.760838 | 0.800172 | 0.833429 | 0.856671 | 0.879914 | |
| lambdaMediaAds | 723 | 0.527552 | 0.796971 | 0.969516 | 1.122863 | 1.281729 | 1.412877 | 1.516404 | 1.592324 | 1.654440 | 1.709654 | 1.731743 | 1.780055 | 1.821466 | 1.918105 | |
| United States | FaceBoom | 27349 | 0.585464 | 0.660355 | 0.699989 | 0.727945 | 0.746376 | 0.759514 | 0.773965 | 0.784076 | 0.791595 | 0.800026 | 0.805537 | 0.811558 | 0.816670 | 0.820684 |
| MediaTornado | 4148 | 0.138115 | 0.192493 | 0.248074 | 0.295012 | 0.333770 | 0.368660 | 0.390313 | 0.420391 | 0.442049 | 0.463703 | 0.487765 | 0.510622 | 0.528667 | 0.536128 | |
| RocketSuperAds | 4225 | 0.208847 | 0.319420 | 0.397370 | 0.456424 | 0.516424 | 0.569808 | 0.638334 | 0.685579 | 0.736622 | 0.787893 | 0.829467 | 0.862540 | 0.894665 | 0.930097 | |
| TipTop | 18367 | 0.343283 | 0.497948 | 0.624789 | 0.739948 | 0.842660 | 0.936131 | 1.014657 | 1.097478 | 1.172419 | 1.246489 | 1.312076 | 1.371739 | 1.428313 | 1.489010 | |
| YRabbit | 4077 | 0.163078 | 0.212284 | 0.253897 | 0.302124 | 0.354778 | 0.386600 | 0.413527 | 0.438008 | 0.460039 | 0.479622 | 0.501653 | 0.523689 | 0.545965 | 0.559429 |
print('roi_region_channel')
disp_no_sort(roi_region_channel)
roi_region_channel
| cohort_size | cac | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| region | channel | ||||||||||||||||
| France | AdNonSense | 1305 | $ 1.01 | 46.5% | 52.3% | 58.0% | 61.8% | 63.7% | 67.6% | 68.3% | 69.5% | 69.8% | 71.0% | 71.4% | 72.6% | 73.3% | 75.2% |
| LeapBob | 2818 | $ 0.21 | 46.7% | 70.3% | 93.1% | 106.7% | 119.4% | 132.0% | 138.8% | 144.7% | 168.3% | 180.9% | 190.2% | 199.5% | 208.8% | 213.8% | |
| OppleCreativeMedia | 2811 | $ 0.25 | 37.1% | 52.0% | 63.4% | 78.3% | 89.9% | 99.1% | 108.3% | 115.6% | 121.3% | 129.1% | 135.5% | 139.8% | 144.7% | 149.0% | |
| WahooNetBanner | 2813 | $ 0.60 | 32.5% | 49.8% | 61.1% | 71.4% | 83.8% | 94.4% | 103.0% | 108.9% | 115.4% | 122.7% | 130.4% | 136.3% | 144.0% | 148.7% | |
| lambdaMediaAds | 707 | $ 0.72 | 64.4% | 81.2% | 93.9% | 113.4% | 133.0% | 149.6% | 162.2% | 175.9% | 187.8% | 195.6% | 209.5% | 215.3% | 226.1% | 235.8% | |
| Germany | AdNonSense | 1151 | $ 1.01 | 52.6% | 62.0% | 67.6% | 71.5% | 74.6% | 76.3% | 78.5% | 79.7% | 84.5% | 86.6% | 87.5% | 87.9% | 90.1% | 90.9% |
| LeapBob | 2361 | $ 0.21 | 41.4% | 66.6% | 88.7% | 104.8% | 115.0% | 130.1% | 141.4% | 151.4% | 156.5% | 170.5% | 180.6% | 188.6% | 193.7% | 200.7% | |
| OppleCreativeMedia | 2413 | $ 0.25 | 31.6% | 42.4% | 66.4% | 78.8% | 92.8% | 102.8% | 112.7% | 121.1% | 123.6% | 130.2% | 139.5% | 145.5% | 145.5% | 151.3% | |
| WahooNetBanner | 2425 | $ 0.60 | 29.5% | 46.4% | 61.1% | 73.4% | 86.4% | 94.6% | 102.5% | 113.5% | 123.7% | 132.6% | 142.2% | 149.1% | 157.6% | 163.9% | |
| lambdaMediaAds | 619 | $ 0.73 | 52.3% | 74.4% | 93.3% | 104.3% | 118.7% | 133.2% | 148.7% | 162.0% | 180.8% | 190.8% | 205.2% | 214.0% | 225.1% | 230.6% | |
| UK | AdNonSense | 1231 | $ 1.01 | 52.2% | 62.3% | 67.6% | 71.6% | 73.6% | 75.6% | 79.2% | 80.8% | 82.5% | 83.3% | 83.3% | 84.1% | 84.9% | 85.3% |
| LeapBob | 2868 | $ 0.21 | 62.4% | 83.1% | 108.8% | 122.2% | 134.0% | 147.3% | 166.3% | 178.1% | 198.0% | 207.1% | 220.4% | 232.0% | 243.6% | 253.6% | |
| OppleCreativeMedia | 2854 | $ 0.25 | 47.0% | 62.4% | 76.7% | 91.4% | 107.5% | 118.7% | 128.5% | 136.3% | 150.3% | 158.0% | 163.6% | 168.5% | 184.6% | 191.6% | |
| WahooNetBanner | 2791 | $ 0.60 | 33.0% | 48.9% | 66.2% | 76.3% | 87.3% | 94.8% | 109.1% | 115.6% | 122.2% | 126.4% | 132.9% | 138.5% | 142.3% | 146.2% | |
| lambdaMediaAds | 723 | $ 0.73 | 72.3% | 109.2% | 132.8% | 153.8% | 175.5% | 193.5% | 207.7% | 218.1% | 226.6% | 234.1% | 237.2% | 243.8% | 249.5% | 262.7% | |
| United States | FaceBoom | 27349 | $ 1.11 | 52.6% | 59.3% | 62.9% | 65.4% | 67.0% | 68.2% | 69.5% | 70.4% | 71.1% | 71.9% | 72.3% | 72.9% | 73.3% | 73.7% |
| MediaTornado | 4148 | $ 0.22 | 63.0% | 87.8% | 113.2% | 134.6% | 152.2% | 168.2% | 178.0% | 191.8% | 201.6% | 211.5% | 222.5% | 232.9% | 241.1% | 244.6% | |
| RocketSuperAds | 4225 | $ 0.42 | 50.0% | 76.4% | 95.0% | 109.2% | 123.5% | 136.3% | 152.7% | 164.0% | 176.2% | 188.5% | 198.4% | 206.3% | 214.0% | 222.5% | |
| TipTop | 18367 | $ 2.75 | 12.5% | 18.1% | 22.7% | 26.9% | 30.6% | 34.0% | 36.9% | 39.9% | 42.6% | 45.3% | 47.7% | 49.9% | 51.9% | 54.1% | |
| YRabbit | 4077 | $ 0.22 | 75.1% | 97.8% | 117.0% | 139.2% | 163.5% | 178.1% | 190.5% | 201.8% | 212.0% | 221.0% | 231.1% | 241.3% | 251.6% | 257.8% |
Вывод. Окупаемость рекламы с разбивкой по странам и рекламным каналам¶
Из-за нерентабельности канала нужно понизить ставки на рекламные объявления или отказаться от услуг каналов:
- AdNonSense для регионов Франция ROI 75.2%, Германия ROI 90.9%, Великобритания ROI 85.3%;
- TipTop для США ROI 54.1%;
- FaceBoom для США 73.7%.
Вывод. Окупаемость рекламы¶
В целом реклама, направленная на привлечение пользователей, не окупается.
Связано это с закупкой большого количества пользователей через нерентабельные каналы продвижения TipTop, FaceBoom, AdNonSense, которые дают 86.4% бюджета на продвижение с ROI 14-го дня 0.54 - 0.83 при норме ROI больше единицы.
Нерентабельные пользователи закупаются из США - 66.7% новых клиентов, пользуются устройствами Mac, iPhone, Android.
Вероятно, проблемы окупаемости связаны с нечетким таргетированием рекламных объявлений, из-за чего на аукционах возникает большая конкуренция. Вероятно, в объявлениях не установлен верхний порог ценового предложения, которое мы готовы рентабельно осилить. Также, вероятно, не проводилось сравнение отдачи от пользователей и стоимости их закупки.
Рекомендации отделу маркетинга
- Понизить ставки по каналу продвижения TipTop, FaceBoom , AdNonSense так как эти каналы вместе дают 86.4% бюджета на продвижение и вместо ROI больше единицы имеют значения на 14-й день 0.54 - 0.83.
- Увеличить финансирование прочих каналов продвижения, так как по ним привлекается 27.3% пользователей и ROI на четырнадцатый день составляет 1.52 - 2.57.
- Поработать над низкочастотными поисковыми запросами, так как по ним ставки ниже на аукционах.
1. Общий вывод¶
Проведен анализ рекламы мобильного приложения.
Рассмотрен промежуток с 1 мая по 1 ноября 2019 года.
Выявлено, что в целом реклама, направленная на привлечение пользователей, не окупается. При этом рекламный бюджет вырос с 10 тыс. долларов в мае до 210 тыс. долларов в октябре.
Общая сумма расходов на маркетинг составила $ 105 497.30.
За отчетный период впервые привлечено 150 008 пользователей.
Пользователи пришли в приложение из четырех стран: США - 66.7% (100 002 человека), Великобритании - 11.7% (17 575 человек), Франции - 11.6% (17 450 человек), Германии - 10% (14 981 человек).
Больше всего платящих пользователей дали США - 6902 человек или 77.7%. Великобритания дала 700 человек 7.9%, Франции - 663 человека 7.5% , Германии - 616 человек 6.9%.
Конверсия обычных пользователей в платящих в США составляет 6.9%, в Великобритании 4%, Франции 3.8%, Германии 4.1%.
Клиенты пользуются устройствами iPhone 36.3%, Android 23.4%, PC 20.3%, Mac 20%.
Платящие клиенты предпочитают iPhone 38.1%, Android 23.1%, Mac 21.5%, PC 17.3%. Всего мобильных клиентов 61.2%.
Конверсия пользователей в платящих пользователей по устройствам составляет: Mac 6.4%, iPhone 6.2%, Android 5.9%, PC 5%.
Удержание платящих пользователей в первые пять дней в 3 - 4 раз выше чем неплатящих.
На 14-й день удержание неплатящих пользователей стремится к нулю, удержание платящих сохраняется на уровне около 10%.
Всего используется 11 каналов продвижения.
Стоимость привлечения пользователей для Франции, Германии, Великобритании соизмерима и составила 0.65 доллара в мае и затем понизилась до 0.45 долларов.
Стоимость привлечения для США с 0.8 доллара в начале мая достигла 1.2 в начале июня и далее продолжила расти до 1.7 - 1.8 доллара в сентябре, октябре.
В целом в заданные бизнес-планом сроки реклама не окупается. Реклама окупалась в мае 2019 года и в середине июня. В остальные промежутки времени окупаемости рекламы в четырнадцати дневный срок не наблюдается.
В среднем пользователи конвертируются в платящих 8% случаев.
Причины неэффективности привлечения пользователей.
Причиной неэффективности привлечения пользователей является увеличение ставок на аукционах по закупке пользователей, выполненное через каналы продвижения TipTop, FaceBoom для рынка США с мая по октябрь 2019 года и приведшее к нарушению возврата инвестиций, выделенных на рекламу. Видимо, в компании не проводился анализ возврата инвестиций по каналам продвижения.
Три канала продвижения не обеспечивают возврата инвестиций в предусмотренный бизнес-планом четырнадцати дневный срок: TipTop, FaceBoom, AdNonSense.
Ключевые метрики:
Бюджет неэффективных каналов привлечения: - TipTop $ 54 751.30 - 51.9% всего бюджета на продвижение
FaceBoom $ 32 445.60 - 30.8%
AdNonSense $3 911.25 - 3.7%
Количество привлеченных пользователей:
TipTop 19 561 — 13.0% от общего числа привлеченных пользователей
FaceBoom 29 144 - 19.4%
AdNonSense 3880 — 2.6%
Количество привлеченных платящих пользователей:
- TipTop 1878 - 21.1% от общего числа платящих пользователей
- FaceBoom 3557 - 40.1%
- AdNonSense 440 — 5.0%
Конверсия в платящих пользователей по каналам
- TipTop 2.4%
- FaceBoom 7.7%
- AdNonSense 11.3%
ROI 14-го дня по каналам:
- TipTop 54.1%
- FaceBoom 73.7%
- AdNonSense 83.5%
LTV 14-го дня:
TipTop $ 1.49
FaceBoom $ 0.82
AdNonSense $ 0.84
Стоимость привлечения одного пользователя по каналу:
TipTop $ 2.8
FaceBoom $1.1
AdNonSense $1.01
Период окупаемости пользователя:
TipTop 27 дней
FaceBoom 19 дней
AdNonSense 17 дней
Удержание пользователей по каналам на 14-й день:
TipTop 20.7%
FaceBoom 1.3%
AdNonSense 2.7%
Соответственно большая часть пользователей, привлеченных по этим каналам «не доживает» до момента окупаемости вложенных в рекламу средств.
Рекомендации отделу маркетинга
- Понизить ставки по каналу продвижения TipTop, FaceBoom , AdNonSense, так как эти каналы вместе дают 86.4% бюджета на продвижение и вместо ROI больше 100% имеют значения на 14-й день 54.1% — 83.5%
- Увеличить финансирование прочих каналов продвижения, так как по ним привлекается 27.3% пользователей и ROI на четырнадцатый день составляет 152% - 257%.
- Поработать над низкочастотными поисковыми запросами, так как по ним ставки ниже на аукционах.